@tangle-network/agent-gateway 0.5.0 → 0.7.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.
@@ -0,0 +1,1703 @@
1
+ import {
2
+ MemoryNonceStore
3
+ } from "./chunk-M7ZJAK4K.js";
4
+ import {
5
+ MemoryRateLimitStore,
6
+ checkRateLimit
7
+ } from "./chunk-XCTXHZ76.js";
8
+
9
+ // src/middleware.ts
10
+ import { Hono } from "hono";
11
+
12
+ // src/filter.ts
13
+ var INJECTION_PATTERNS = [
14
+ // Direct instruction override
15
+ /ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|rules?|directives?)/i,
16
+ /disregard\s+(all\s+)?(previous|prior|system)/i,
17
+ /forget\s+(everything|all|your)\s+(previous|instructions?|training)/i,
18
+ // Role assumption
19
+ /you\s+are\s+now\s+(a|an|the)\s+/i,
20
+ /pretend\s+(you\s+are|to\s+be)\s+/i,
21
+ /act\s+as\s+(if\s+you\s+are|a|an|the)\s+/i,
22
+ /new\s+instructions?:/i,
23
+ /\[system\]/i,
24
+ /\[INST\]/i,
25
+ // Prompt extraction
26
+ /what\s+(is|are)\s+your\s+(system\s+)?(prompt|instructions?|rules?|directives?)/i,
27
+ /repeat\s+(your|the)\s+(system\s+)?(prompt|instructions?)/i,
28
+ /output\s+(your|the)\s+((system|initial|original|first|full|real|hidden|secret|raw|exact)\s+)?(prompt|instructions?)/i,
29
+ /show\s+(me\s+)?(your|the)\s+((system|hidden|secret|initial|original)\s+)?(prompt|instructions?|message|directives?)/i,
30
+ /tell\s+me\s+(your|the)\s+(system\s+)?(prompt|instructions?|rules?)/i,
31
+ // Developer/debug/admin/jailbreak mode override
32
+ /(developer|debug|admin|god|sudo|jailbreak|unrestricted|maintenance)\s+mode/i,
33
+ // Safety/safeguard disablement
34
+ /(disable|bypass|override|remove|turn\s+off)\s+(your\s+|the\s+)?(safety|safeguards?|guardrails?|filters?|restrictions?|rules?|limitations?|content\s+policy)/i,
35
+ // Role reversal
36
+ /(from\s+now\s+on|starting\s+now|now)\s+you('re|\s+are)\s+(the\s+)?(user|human|customer|assistant)/i,
37
+ /i('m|\s+am)\s+(the\s+)?(assistant|ai|model|llm|bot)/i,
38
+ // Tool-call / JSON-shaped spoofs
39
+ /"(?:tool|function|action|call)"\s*:\s*"[^"]*(exfiltrate|leak|reveal|dump|extract|steal)[^"]*"/i,
40
+ /exfiltrate[_\s]*(system|prompt|secret|vault|config|data)/i,
41
+ // Data exfiltration
42
+ /read\s+(the\s+)?(vault|workspace|config|secret|\.env)/i,
43
+ /cat\s+\/home\/agent\/(vault|config|\.env|secrets?)/i,
44
+ /list\s+(all\s+)?(vault|workspace|secret)\s+(files?|contents?|data)/i,
45
+ // i18n — Spanish
46
+ /ignor[ae]\s+(todas?\s+)?(las\s+)?(instrucciones?|indicaciones?|órdenes?)\s+(previas?|anteriores?)/i,
47
+ /olvid[ae]\s+(todas?\s+|tus\s+)?(instrucciones?|indicaciones?)/i,
48
+ /(ahora\s+)?(tú\s+)?eres\s+(ahora\s+)?(un|una|el|la)\s+/i,
49
+ /finge\s+(que\s+eres|ser)\s+/i,
50
+ // i18n — French
51
+ /ignor(ez|e|es)\s+(toutes?\s+)?(les\s+)?(instructions?|consignes?|directives?)\s+(précédentes?|antérieures?)/i,
52
+ /oublie(z|s)?\s+(toutes?\s+|vos\s+|tes\s+)?(instructions?|consignes?)/i,
53
+ /(vous\s+êtes|tu\s+es)\s+(maintenant|désormais)\s+(un|une)\s+/i,
54
+ /prétend(ez|s)?\s+(être|que\s+vous\s+êtes|que\s+tu\s+es)\s+/i,
55
+ // i18n — German
56
+ /ignorier(e|en|t)\s+(alle\s+)?(vorherigen?|vorigen?|bisherigen?)\s+(anweisungen|anordnungen|vorgaben)/i,
57
+ /vergiss\s+(alle\s+|deine\s+)?(anweisungen|vorgaben|regeln)/i,
58
+ /du\s+bist\s+(jetzt|nun)\s+(ein|eine)\s+/i,
59
+ /tu\s+so\s+als\s+(wärst\s+du|ob\s+du)\s+/i
60
+ ];
61
+ var HOMOGLYPHS = {
62
+ "\u0430": "a",
63
+ "\u0435": "e",
64
+ "\u043E": "o",
65
+ "\u0440": "p",
66
+ "\u0441": "c",
67
+ "\u0443": "y",
68
+ "\u0445": "x",
69
+ "\u0456": "i",
70
+ "\u0458": "j",
71
+ "\u0455": "s",
72
+ "\u04BB": "h",
73
+ "\u0410": "A",
74
+ "\u0412": "B",
75
+ "\u0415": "E",
76
+ "\u041A": "K",
77
+ "\u041C": "M",
78
+ "\u041D": "H",
79
+ "\u041E": "O",
80
+ "\u0420": "P",
81
+ "\u0421": "C",
82
+ "\u0422": "T",
83
+ "\u0425": "X",
84
+ "\u0406": "I",
85
+ "\u0408": "J",
86
+ "\u0405": "S",
87
+ "\u03B1": "a",
88
+ "\u03B5": "e",
89
+ "\u03BF": "o",
90
+ "\u03C1": "p",
91
+ "\u03C5": "y",
92
+ "\u03BD": "v",
93
+ "\u03B9": "i",
94
+ "\u03C4": "t",
95
+ "\u03BA": "k",
96
+ "\u03C7": "x",
97
+ "\u03B7": "n",
98
+ "\u03BC": "u",
99
+ "\u0391": "A",
100
+ "\u0392": "B",
101
+ "\u0395": "E",
102
+ "\u0396": "Z",
103
+ "\u0397": "H",
104
+ "\u0399": "I",
105
+ "\u039A": "K",
106
+ "\u039C": "M",
107
+ "\u039D": "N",
108
+ "\u039F": "O",
109
+ "\u03A1": "P",
110
+ "\u03A4": "T",
111
+ "\u03A7": "X",
112
+ "\u03A5": "Y"
113
+ };
114
+ function collapseHomoglyphs(text) {
115
+ let out = "";
116
+ for (const ch of text) out += HOMOGLYPHS[ch] ?? ch;
117
+ return out;
118
+ }
119
+ function normalizeUnicode(text) {
120
+ return collapseHomoglyphs(
121
+ text.replace(/[\u200B-\u200F\u2028-\u202F\u2060\uFEFF]/g, "").normalize("NFKC")
122
+ );
123
+ }
124
+ function normalizeForDetection(text) {
125
+ const spaced = text.replace(/[\u200B\u200C\uFEFF]/g, " ").replace(/[\u200D\u200E\u200F\u2028-\u202F\u2060]/g, "").normalize("NFKC");
126
+ return collapseHomoglyphs(spaced).replace(/[(){}<>,.!?:;"`~]+/g, " ").replace(/\s+/g, " ");
127
+ }
128
+ function normalizeForDetectionNoBrackets(text) {
129
+ return normalizeForDetection(text).replace(/[\[\]]+/g, " ").replace(/\s+/g, " ");
130
+ }
131
+ function detectInjection(content) {
132
+ const stripNorm = normalizeUnicode(content);
133
+ const detectNorm = normalizeForDetection(content);
134
+ const noBracketNorm = normalizeForDetectionNoBrackets(content);
135
+ const matches = [];
136
+ for (const pattern of INJECTION_PATTERNS) {
137
+ if (pattern.test(stripNorm) || pattern.test(detectNorm) || pattern.test(noBracketNorm)) {
138
+ matches.push(pattern.source.slice(0, 60));
139
+ }
140
+ }
141
+ for (const norm of [stripNorm, detectNorm]) {
142
+ const b64Matches = norm.match(/[A-Za-z0-9+/]{40,}={0,2}/g);
143
+ if (!b64Matches) continue;
144
+ for (const b64 of b64Matches) {
145
+ try {
146
+ const decoded = atob(b64);
147
+ if (INJECTION_PATTERNS.some((p) => p.test(decoded))) {
148
+ if (!matches.includes("base64-encoded injection")) {
149
+ matches.push("base64-encoded injection");
150
+ }
151
+ }
152
+ } catch {
153
+ }
154
+ }
155
+ }
156
+ return matches;
157
+ }
158
+ function filterConsumerMessages(messages, maxLength = 8e3) {
159
+ return messages.filter((m) => m.role !== "system").map((m) => {
160
+ const normalized = normalizeUnicode(m.content);
161
+ const redacted = normalized.replace(/\b(vault|workspace|owner|admin|secret|\.env|config\.json)[\s/:][^\s]*/gi, "[REDACTED]").slice(0, maxLength);
162
+ return { role: m.role, content: redacted };
163
+ });
164
+ }
165
+ function filterConsumerMessagesStrict(messages, maxLength = 8e3) {
166
+ const filtered = filterConsumerMessages(messages, maxLength);
167
+ const allContent = filtered.map((m) => m.content).join(" ");
168
+ const injectionWarnings = detectInjection(allContent);
169
+ return { messages: filtered, injectionWarnings };
170
+ }
171
+ function redactSystemPromptFromOutput(output, systemPrompt) {
172
+ if (!systemPrompt || systemPrompt.length < 40) return output;
173
+ const chunks = systemPrompt.split(/[.\n]/).map((s) => s.trim()).filter((s) => s.length >= 40);
174
+ let redacted = output;
175
+ for (const chunk of chunks) {
176
+ const idx = redacted.toLowerCase().indexOf(chunk.toLowerCase());
177
+ if (idx >= 0) {
178
+ redacted = redacted.slice(0, idx) + "[REDACTED \u2014 system instructions]" + redacted.slice(idx + chunk.length);
179
+ }
180
+ }
181
+ return redacted;
182
+ }
183
+
184
+ // src/observer.ts
185
+ var ConsoleObserver = class {
186
+ constructor(log = (e) => console.log(JSON.stringify(e))) {
187
+ this.log = log;
188
+ }
189
+ log;
190
+ emit(level, event, ctx, rest = {}) {
191
+ this.log({
192
+ level,
193
+ event,
194
+ time: (/* @__PURE__ */ new Date()).toISOString(),
195
+ requestId: ctx.requestId,
196
+ agentSlug: ctx.agentSlug,
197
+ durationMs: Date.now() - ctx.startMs,
198
+ ...rest
199
+ });
200
+ }
201
+ onRequestStart(ctx) {
202
+ this.emit("info", "gateway.request.start", ctx);
203
+ }
204
+ onPaymentVerified(ctx, info) {
205
+ this.emit("info", "gateway.payment.verified", ctx, info);
206
+ }
207
+ onAuthFailure(ctx, reason) {
208
+ this.emit("warn", "gateway.auth.failure", ctx, reason);
209
+ }
210
+ onRateLimited(ctx, info) {
211
+ this.emit("warn", "gateway.rate_limit", ctx, info);
212
+ }
213
+ onBodyTooLarge(ctx, contentLength) {
214
+ this.emit("warn", "gateway.body_too_large", ctx, { contentLength });
215
+ }
216
+ onInjectionDetected(ctx, info) {
217
+ this.emit("warn", "gateway.injection", ctx, info);
218
+ }
219
+ onRequestComplete(ctx, usage) {
220
+ this.emit("info", "gateway.request.complete", ctx, usage);
221
+ }
222
+ onStreamError(ctx, info) {
223
+ this.emit("error", "gateway.stream.error", ctx, info);
224
+ }
225
+ onSettlementError(ctx, info) {
226
+ this.emit("error", "gateway.settlement.error", ctx, info);
227
+ }
228
+ };
229
+ var CompositeObserver = class {
230
+ constructor(observers) {
231
+ this.observers = observers;
232
+ }
233
+ observers;
234
+ async fanOut(event, ...args) {
235
+ for (const obs of this.observers) {
236
+ const fn = obs[event];
237
+ if (!fn) continue;
238
+ try {
239
+ await fn.apply(obs, args);
240
+ } catch (err) {
241
+ console.warn(`[agent-gateway] observer ${event} threw:`, err instanceof Error ? err.message : err);
242
+ }
243
+ }
244
+ }
245
+ onRequestStart = (ctx) => this.fanOut("onRequestStart", ctx);
246
+ onPaymentVerified = (ctx, info) => this.fanOut("onPaymentVerified", ctx, info);
247
+ onAuthFailure = (ctx, reason) => this.fanOut("onAuthFailure", ctx, reason);
248
+ onRateLimited = (ctx, info) => this.fanOut("onRateLimited", ctx, info);
249
+ onBodyTooLarge = (ctx, contentLength) => this.fanOut("onBodyTooLarge", ctx, contentLength);
250
+ onInjectionDetected = (ctx, info) => this.fanOut("onInjectionDetected", ctx, info);
251
+ onRequestComplete = (ctx, usage) => this.fanOut("onRequestComplete", ctx, usage);
252
+ onStreamError = (ctx, info) => this.fanOut("onStreamError", ctx, info);
253
+ onSettlementError = (ctx, info) => this.fanOut("onSettlementError", ctx, info);
254
+ };
255
+ function generateRequestId() {
256
+ const bytes = new Uint8Array(16);
257
+ globalThis.crypto.getRandomValues(bytes);
258
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
259
+ return `req_${hex}`;
260
+ }
261
+
262
+ // src/verify.ts
263
+ async function verifyX402(spendAuthHeader, config, nonceStore) {
264
+ try {
265
+ const raw = JSON.parse(spendAuthHeader);
266
+ if (!raw.commitment || !raw.signature || !raw.amount) return null;
267
+ if (raw.operator?.toLowerCase() !== config.operatorAddress.toLowerCase()) return null;
268
+ const amount = BigInt(raw.amount);
269
+ const nonce = BigInt(raw.nonce);
270
+ const expiry = BigInt(raw.expiry);
271
+ if (expiry < BigInt(Math.floor(Date.now() / 1e3))) return null;
272
+ if (amount <= 0n) return null;
273
+ const nonceKey = `${raw.commitment}:${nonce.toString()}`;
274
+ if (nonceStore) {
275
+ if (await nonceStore.hasSeen(nonceKey)) return null;
276
+ const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1e3), 3600);
277
+ await nonceStore.markSeen(nonceKey, Math.max(ttl, 60));
278
+ }
279
+ if (config.verifySigner) {
280
+ const verified = await config.verifySigner(raw);
281
+ if (!verified) return null;
282
+ } else if (!config.demoMode) {
283
+ return null;
284
+ }
285
+ return raw.commitment;
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+ async function verifyMpp(authHeader, _config, x402Config) {
291
+ const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i);
292
+ if (!match) return null;
293
+ const [, , credentialB64] = match;
294
+ try {
295
+ const decoded = Buffer.from(credentialB64, "base64url").toString("utf-8");
296
+ const credential = JSON.parse(decoded);
297
+ const payload = credential.payload ?? credential;
298
+ if (!payload.commitment && !payload.from) return null;
299
+ const operator = payload.operator ?? payload.to;
300
+ if (operator && operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) return null;
301
+ if (payload.amount) BigInt(payload.amount);
302
+ if (payload.nonce) BigInt(payload.nonce);
303
+ return payload.commitment ?? payload.from ?? null;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+ async function defaultVerifyApiKey(authHeader) {
309
+ if (!authHeader.startsWith("Bearer sk_agent_")) return null;
310
+ const key = authHeader.slice(7);
311
+ return {
312
+ keyId: key.slice(0, 16),
313
+ consumerId: `apikey:${key.slice(0, 16)}`
314
+ };
315
+ }
316
+
317
+ // src/dispatch.ts
318
+ async function authenticateAndGuard(c, slug, messages, config, state) {
319
+ const startMs = Date.now();
320
+ const requestId = generateRequestId();
321
+ const ctx = { requestId, agentSlug: slug, startMs };
322
+ await state.obs?.onRequestStart?.(ctx);
323
+ const agent = await config.resolveAgent(slug);
324
+ if (!agent) {
325
+ return c.json({ error: { message: "Agent not found", type: "not_found" } }, 404);
326
+ }
327
+ if (!messages?.length) {
328
+ return c.json(
329
+ { error: { message: "messages array required", type: "invalid_request" } },
330
+ 400
331
+ );
332
+ }
333
+ const spendAuthHeader = c.req.header("X-Payment-Signature");
334
+ const authHeader = c.req.header("Authorization") ?? "";
335
+ let consumerId = null;
336
+ let paymentMethod = "none";
337
+ let keyInfo = null;
338
+ if (spendAuthHeader) {
339
+ const signer = await verifyX402(spendAuthHeader, config.x402, state.nonceStore);
340
+ if (!signer) {
341
+ await state.obs?.onAuthFailure?.(ctx, {
342
+ method: "x402",
343
+ code: "invalid_spend_auth",
344
+ httpStatus: 402
345
+ });
346
+ return c.json(
347
+ {
348
+ error: {
349
+ message: "Invalid X-Payment-Signature",
350
+ type: "authentication_error",
351
+ code: "invalid_spend_auth"
352
+ }
353
+ },
354
+ {
355
+ status: 402,
356
+ headers: { "X-Payment-Required": "spendauth", "X-Request-Id": requestId }
357
+ }
358
+ );
359
+ }
360
+ consumerId = signer;
361
+ paymentMethod = "x402";
362
+ } else if (config.mpp && authHeader.toLowerCase().startsWith("payment ")) {
363
+ const signer = await verifyMpp(authHeader, config.mpp, config.x402);
364
+ if (!signer) {
365
+ const realm = config.mpp.realm;
366
+ const method = config.mpp.method ?? "blueprintevm";
367
+ await state.obs?.onAuthFailure?.(ctx, {
368
+ method: "mpp",
369
+ code: "invalid_mpp_credential",
370
+ httpStatus: 401
371
+ });
372
+ return c.json(
373
+ {
374
+ error: {
375
+ message: "Invalid Payment credential",
376
+ type: "authentication_error",
377
+ code: "invalid_mpp_credential"
378
+ }
379
+ },
380
+ {
381
+ status: 401,
382
+ headers: {
383
+ "WWW-Authenticate": `Payment realm="${realm}", method="${method}"`,
384
+ "X-Request-Id": requestId
385
+ }
386
+ }
387
+ );
388
+ }
389
+ consumerId = signer;
390
+ paymentMethod = "mpp";
391
+ } else if (authHeader.startsWith("Bearer ")) {
392
+ const verify = config.verifyApiKey ?? defaultVerifyApiKey;
393
+ const key = await verify(authHeader);
394
+ if (!key) {
395
+ await state.obs?.onAuthFailure?.(ctx, {
396
+ method: "apikey",
397
+ code: "invalid_api_key",
398
+ httpStatus: 401
399
+ });
400
+ return c.json(
401
+ { error: { message: "Invalid API key", type: "authentication_error" } },
402
+ { status: 401, headers: { "X-Request-Id": requestId } }
403
+ );
404
+ }
405
+ if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(state.requiredScope)) {
406
+ await state.obs?.onAuthFailure?.(ctx, {
407
+ method: "apikey",
408
+ code: "insufficient_scope",
409
+ httpStatus: 403
410
+ });
411
+ return c.json(
412
+ {
413
+ error: {
414
+ message: `API key missing required scope: ${state.requiredScope}`,
415
+ type: "forbidden",
416
+ code: "insufficient_scope"
417
+ }
418
+ },
419
+ { status: 403, headers: { "X-Request-Id": requestId } }
420
+ );
421
+ }
422
+ consumerId = key.consumerId;
423
+ paymentMethod = "apikey";
424
+ keyInfo = key;
425
+ } else {
426
+ await state.obs?.onAuthFailure?.(ctx, {
427
+ method: "none",
428
+ code: "payment_required",
429
+ httpStatus: 402
430
+ });
431
+ const methods = ["x402"];
432
+ if (config.mpp) methods.push("mpp");
433
+ methods.push("api_key");
434
+ const headers = {
435
+ "X-Payment-Required": methods.join(", "),
436
+ "X-Request-Id": requestId
437
+ };
438
+ if (config.mpp) {
439
+ headers["WWW-Authenticate"] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? "blueprintevm"}"`;
440
+ }
441
+ return c.json(
442
+ {
443
+ error: {
444
+ message: "Payment required",
445
+ type: "payment_required",
446
+ payment_methods: methods,
447
+ x402: {
448
+ operator: config.x402.operatorAddress,
449
+ chain_id: config.x402.chainId,
450
+ credits_address: config.x402.creditsAddress,
451
+ estimated_amount_per_request: "20000"
452
+ },
453
+ ...config.mpp ? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? "blueprintevm" } } : {},
454
+ api_key: {
455
+ purchase_url: config.baseUrl ? `${config.baseUrl}/agents/${slug}/api-keys` : void 0
456
+ }
457
+ }
458
+ },
459
+ { status: 402, headers }
460
+ );
461
+ }
462
+ await state.obs?.onPaymentVerified?.(ctx, {
463
+ method: paymentMethod,
464
+ consumerId,
465
+ keyId: keyInfo?.keyId
466
+ });
467
+ const effectiveRateLimit = keyInfo?.rateLimitPerMinute ? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 } : state.globalRateLimit;
468
+ const rl = await checkRateLimit(consumerId, effectiveRateLimit, state.rateLimitStore);
469
+ if (!rl.allowed) {
470
+ await state.obs?.onRateLimited?.(ctx, {
471
+ consumerId,
472
+ retryAfterSeconds: rl.retryAfterSeconds ?? 60
473
+ });
474
+ return c.json(
475
+ {
476
+ error: {
477
+ message: "Rate limit exceeded",
478
+ type: "rate_limit_error",
479
+ retry_after: rl.retryAfterSeconds
480
+ }
481
+ },
482
+ {
483
+ status: 429,
484
+ headers: {
485
+ "Retry-After": String(rl.retryAfterSeconds ?? 60),
486
+ "X-Request-Id": requestId
487
+ }
488
+ }
489
+ );
490
+ }
491
+ const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(
492
+ messages,
493
+ state.maxLen
494
+ );
495
+ if (injectionWarnings.length > 0) {
496
+ await state.obs?.onInjectionDetected?.(ctx, {
497
+ consumerId,
498
+ patterns: injectionWarnings,
499
+ blocked: !!config.blockInjection
500
+ });
501
+ if (config.blockInjection) {
502
+ return c.json(
503
+ {
504
+ error: {
505
+ message: "Request rejected: potential prompt injection detected",
506
+ type: "content_policy_violation"
507
+ }
508
+ },
509
+ { status: 400, headers: { "X-Request-Id": requestId } }
510
+ );
511
+ }
512
+ }
513
+ const userMessage = filtered.filter((m) => m.role === "user").map((m) => m.content).join("\n\n");
514
+ if (!userMessage) {
515
+ return c.json(
516
+ { error: { message: "No user message provided", type: "invalid_request" } },
517
+ 400
518
+ );
519
+ }
520
+ if (config.authorizeConsumer) {
521
+ const authz = await config.authorizeConsumer(agent, {
522
+ method: paymentMethod,
523
+ consumerId,
524
+ keyId: keyInfo?.keyId,
525
+ requestId
526
+ });
527
+ if (!authz.allow) {
528
+ return c.json(
529
+ {
530
+ error: {
531
+ message: authz.reason,
532
+ type: "authorization_denied",
533
+ code: authz.code
534
+ }
535
+ },
536
+ { status: 403, headers: { "X-Request-Id": requestId } }
537
+ );
538
+ }
539
+ }
540
+ return {
541
+ agent,
542
+ consumerId,
543
+ paymentMethod,
544
+ keyInfo,
545
+ userMessage,
546
+ rateLimitRemaining: rl.remaining,
547
+ requestId,
548
+ startMs
549
+ };
550
+ }
551
+ async function* dispatchSandboxStream(agent, userMessage, consumerId, config, signal, sessionId) {
552
+ for await (const event of dispatchSandboxStreamRich(
553
+ agent,
554
+ userMessage,
555
+ consumerId,
556
+ config,
557
+ signal,
558
+ sessionId
559
+ )) {
560
+ if (event.kind === "text") yield event.delta;
561
+ }
562
+ }
563
+ async function* dispatchSandboxStreamRich(agent, userMessage, consumerId, config, signal, sessionId) {
564
+ const box = await config.getSandbox(agent);
565
+ const promptStream = box.streamPrompt(userMessage, {
566
+ sessionId: sessionId ?? `consumer:${consumerId}`,
567
+ systemPrompt: agent.systemPrompt
568
+ });
569
+ for await (const event of promptStream) {
570
+ if (signal?.aborted) return;
571
+ if (event.type === "message.part.updated" && event.data?.part?.type === "text" && event.data.delta) {
572
+ yield {
573
+ kind: "text",
574
+ delta: redactSystemPromptFromOutput(event.data.delta, agent.systemPrompt)
575
+ };
576
+ continue;
577
+ }
578
+ if (event.type === "input-required" || event.data?.inputRequired) {
579
+ yield { kind: "input-required", prompt: event.data?.inputRequired?.prompt };
580
+ return;
581
+ }
582
+ }
583
+ }
584
+ async function settleAndRecord(agent, authz, inputTokens, outputTokens, config, obs) {
585
+ const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd;
586
+ const ownerEarned = totalCost * (1 - agent.platformFeePercent);
587
+ const platformFee = totalCost * agent.platformFeePercent;
588
+ const usageEvent = {
589
+ requestId: authz.requestId,
590
+ agentId: agent.id,
591
+ agentSlug: agent.slug,
592
+ consumerId: authz.consumerId,
593
+ paymentMethod: authz.paymentMethod,
594
+ inputTokens,
595
+ outputTokens,
596
+ totalCostUsd: totalCost,
597
+ ownerEarnedUsd: ownerEarned,
598
+ platformFeeUsd: platformFee,
599
+ durationMs: Date.now() - authz.startMs
600
+ };
601
+ await config.recordUsage(usageEvent);
602
+ const ctx = {
603
+ requestId: authz.requestId,
604
+ agentSlug: agent.slug,
605
+ startMs: authz.startMs
606
+ };
607
+ await obs?.onRequestComplete?.(ctx, usageEvent);
608
+ if (config.settlePayment) {
609
+ await config.settlePayment(
610
+ {
611
+ method: authz.paymentMethod,
612
+ consumerId: authz.consumerId,
613
+ requestId: authz.requestId
614
+ },
615
+ totalCost
616
+ ).catch(async (err) => {
617
+ const msg = err instanceof Error ? err.message : String(err);
618
+ console.error(`[agent-gateway] settlement failed for ${authz.consumerId}: ${msg}`);
619
+ await obs?.onSettlementError?.(ctx, {
620
+ consumerId: authz.consumerId,
621
+ method: authz.paymentMethod,
622
+ errorMessage: msg
623
+ });
624
+ });
625
+ }
626
+ }
627
+ function estimateTokens(text) {
628
+ return Math.ceil(text.length / 4);
629
+ }
630
+
631
+ // src/a2a/agent-card.ts
632
+ function buildAgentCard(agent, config, agentUrl) {
633
+ const schemes = ["x402"];
634
+ if (config.mpp) schemes.push("mpp");
635
+ schemes.push("Bearer");
636
+ const skills = agent.skills && agent.skills.length > 0 ? agent.skills : [
637
+ {
638
+ id: "chat",
639
+ name: agent.slug,
640
+ description: agent.description ?? `${agent.slug} agent`,
641
+ tags: ["chat"],
642
+ inputModes: ["text"],
643
+ outputModes: ["text"]
644
+ }
645
+ ];
646
+ return {
647
+ name: agent.slug,
648
+ description: agent.description ?? `${agent.slug} agent`,
649
+ url: agentUrl,
650
+ version: "1.0.0",
651
+ provider: { organization: "Tangle", url: "https://tangle.tools" },
652
+ capabilities: {
653
+ streaming: true,
654
+ pushNotifications: !!config.a2a?.pushStore,
655
+ stateTransitionHistory: false
656
+ },
657
+ authentication: { schemes },
658
+ defaultInputModes: ["text"],
659
+ defaultOutputModes: ["text"],
660
+ skills
661
+ };
662
+ }
663
+
664
+ // src/a2a/types.ts
665
+ var A2A_ERROR_CODES = {
666
+ PARSE_ERROR: -32700,
667
+ INVALID_REQUEST: -32600,
668
+ METHOD_NOT_FOUND: -32601,
669
+ INVALID_PARAMS: -32602,
670
+ INTERNAL_ERROR: -32603,
671
+ TASK_NOT_FOUND: -32001,
672
+ TASK_NOT_CANCELABLE: -32002,
673
+ PUSH_NOT_SUPPORTED: -32003,
674
+ UNSUPPORTED_OPERATION: -32004,
675
+ CONTENT_TYPE_NOT_SUPPORTED: -32005,
676
+ INVALID_AGENT_RESPONSE: -32006,
677
+ AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED: -32007
678
+ };
679
+
680
+ // src/a2a/jsonrpc.ts
681
+ function parseEnvelope(raw) {
682
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
683
+ return {
684
+ id: null,
685
+ code: A2A_ERROR_CODES.INVALID_REQUEST,
686
+ message: "request must be a JSON object"
687
+ };
688
+ }
689
+ const req = raw;
690
+ const id = req.id === void 0 ? null : req.id;
691
+ if (req.jsonrpc !== "2.0") {
692
+ return { id, code: A2A_ERROR_CODES.INVALID_REQUEST, message: 'jsonrpc field must be "2.0"' };
693
+ }
694
+ if (typeof req.method !== "string" || req.method.length === 0) {
695
+ return { id, code: A2A_ERROR_CODES.INVALID_REQUEST, message: "method field required" };
696
+ }
697
+ return {
698
+ jsonrpc: "2.0",
699
+ id,
700
+ method: req.method,
701
+ params: req.params
702
+ };
703
+ }
704
+ function ok(id, result) {
705
+ return { jsonrpc: "2.0", id, result };
706
+ }
707
+ function fail(id, code, message, data) {
708
+ return {
709
+ jsonrpc: "2.0",
710
+ id,
711
+ error: { code, message, ...data !== void 0 ? { data } : {} }
712
+ };
713
+ }
714
+
715
+ // src/a2a/push-notifications.ts
716
+ var InMemoryPushNotificationStore = class {
717
+ byTask = /* @__PURE__ */ new Map();
718
+ async set(taskId, config) {
719
+ let configs = this.byTask.get(taskId);
720
+ if (!configs) {
721
+ configs = /* @__PURE__ */ new Map();
722
+ this.byTask.set(taskId, configs);
723
+ }
724
+ configs.set(config.id, { ...config });
725
+ }
726
+ async get(taskId, configId) {
727
+ const cfg = this.byTask.get(taskId)?.get(configId);
728
+ return cfg ? { ...cfg } : void 0;
729
+ }
730
+ async list(taskId) {
731
+ const configs = this.byTask.get(taskId);
732
+ if (!configs) return [];
733
+ return [...configs.values()].map((c) => ({ ...c }));
734
+ }
735
+ async delete(taskId, configId) {
736
+ const configs = this.byTask.get(taskId);
737
+ if (!configs) return;
738
+ configs.delete(configId);
739
+ if (configs.size === 0) this.byTask.delete(taskId);
740
+ }
741
+ };
742
+ var PUSH_TABLE_DDL = (table) => `
743
+ CREATE TABLE IF NOT EXISTS ${table} (
744
+ task_id TEXT NOT NULL,
745
+ config_id TEXT NOT NULL,
746
+ url TEXT NOT NULL,
747
+ token TEXT,
748
+ authentication TEXT,
749
+ PRIMARY KEY (task_id, config_id)
750
+ )
751
+ `;
752
+ var SqlPushNotificationStore = class {
753
+ constructor(db, table = "a2a_push_configs") {
754
+ this.db = db;
755
+ this.table = table;
756
+ }
757
+ db;
758
+ table;
759
+ async migrate() {
760
+ await this.db.exec(PUSH_TABLE_DDL(this.table));
761
+ }
762
+ async set(taskId, config) {
763
+ const auth = config.authentication ? JSON.stringify(config.authentication) : null;
764
+ const updated = await this.db.exec(
765
+ `UPDATE ${this.table} SET url = ?, token = ?, authentication = ? WHERE task_id = ? AND config_id = ?`,
766
+ [config.url, config.token ?? null, auth, taskId, config.id]
767
+ );
768
+ if (updated.rowsAffected === 0) {
769
+ await this.db.exec(
770
+ `INSERT INTO ${this.table} (task_id, config_id, url, token, authentication) VALUES (?, ?, ?, ?, ?)`,
771
+ [taskId, config.id, config.url, config.token ?? null, auth]
772
+ );
773
+ }
774
+ }
775
+ async get(taskId, configId) {
776
+ const rows = await this.db.query(
777
+ `SELECT config_id, url, token, authentication FROM ${this.table} WHERE task_id = ? AND config_id = ?`,
778
+ [taskId, configId]
779
+ );
780
+ const row = rows[0];
781
+ if (!row) return void 0;
782
+ return {
783
+ id: row.config_id,
784
+ url: row.url,
785
+ token: row.token ?? void 0,
786
+ authentication: row.authentication ? JSON.parse(row.authentication) : void 0
787
+ };
788
+ }
789
+ async list(taskId) {
790
+ const rows = await this.db.query(
791
+ `SELECT config_id, url, token, authentication FROM ${this.table} WHERE task_id = ?`,
792
+ [taskId]
793
+ );
794
+ return rows.map((row) => ({
795
+ id: row.config_id,
796
+ url: row.url,
797
+ token: row.token ?? void 0,
798
+ authentication: row.authentication ? JSON.parse(row.authentication) : void 0
799
+ }));
800
+ }
801
+ async delete(taskId, configId) {
802
+ await this.db.exec(
803
+ `DELETE FROM ${this.table} WHERE task_id = ? AND config_id = ?`,
804
+ [taskId, configId]
805
+ );
806
+ }
807
+ };
808
+ async function deliverPushNotifications(args) {
809
+ const fetcher = args.fetcher ?? fetch;
810
+ const configs = await args.store.list(args.task.id);
811
+ const body = JSON.stringify({
812
+ taskId: args.task.id,
813
+ state: args.task.status.state,
814
+ task: args.task
815
+ });
816
+ const signature = args.webhookSecret ? `sha256=${await hmacSha256Hex(args.webhookSecret, body)}` : void 0;
817
+ const results = [];
818
+ for (const config of configs) {
819
+ const headers = { "Content-Type": "application/json" };
820
+ if (config.token) headers["X-A2A-Notification-Token"] = config.token;
821
+ if (signature) headers["X-A2A-Signature"] = signature;
822
+ if (config.authentication?.credentials) {
823
+ const scheme = config.authentication.schemes[0] ?? "Bearer";
824
+ headers.Authorization = `${scheme} ${config.authentication.credentials}`;
825
+ }
826
+ let result;
827
+ try {
828
+ const res = await fetcher(config.url, { method: "POST", headers, body });
829
+ result = {
830
+ taskId: args.task.id,
831
+ configId: config.id,
832
+ url: config.url,
833
+ ok: res.ok,
834
+ status: res.status
835
+ };
836
+ } catch (err) {
837
+ result = {
838
+ taskId: args.task.id,
839
+ configId: config.id,
840
+ url: config.url,
841
+ ok: false,
842
+ error: err instanceof Error ? err.message : String(err)
843
+ };
844
+ }
845
+ args.onDelivery?.(result);
846
+ results.push(result);
847
+ }
848
+ return results;
849
+ }
850
+ async function hmacSha256Hex(secret, body) {
851
+ const enc = new TextEncoder();
852
+ const key = await crypto.subtle.importKey(
853
+ "raw",
854
+ enc.encode(secret),
855
+ { name: "HMAC", hash: "SHA-256" },
856
+ false,
857
+ ["sign"]
858
+ );
859
+ const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
860
+ return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
861
+ }
862
+
863
+ // src/a2a/translate.ts
864
+ function extractTextFromMessage(message) {
865
+ if (!message || message.kind !== "message") {
866
+ return {
867
+ error: {
868
+ code: A2A_ERROR_CODES.INVALID_PARAMS,
869
+ message: 'params.message must be an A2A Message ({ kind: "message", role, parts, messageId })'
870
+ }
871
+ };
872
+ }
873
+ if (!Array.isArray(message.parts) || message.parts.length === 0) {
874
+ return {
875
+ error: {
876
+ code: A2A_ERROR_CODES.INVALID_PARAMS,
877
+ message: "message.parts must be a non-empty array"
878
+ }
879
+ };
880
+ }
881
+ const texts = [];
882
+ for (const part of message.parts) {
883
+ if (part.kind === "text") {
884
+ if (typeof part.text !== "string") {
885
+ return {
886
+ error: {
887
+ code: A2A_ERROR_CODES.INVALID_PARAMS,
888
+ message: "text part .text must be a string"
889
+ }
890
+ };
891
+ }
892
+ texts.push(part.text);
893
+ } else {
894
+ return {
895
+ error: {
896
+ code: A2A_ERROR_CODES.CONTENT_TYPE_NOT_SUPPORTED,
897
+ message: `part.kind '${part.kind}' not supported; this agent accepts text parts only`
898
+ }
899
+ };
900
+ }
901
+ }
902
+ return { text: texts.join("\n\n") };
903
+ }
904
+ function responseTextToArtifact(text, artifactId) {
905
+ return {
906
+ artifactId,
907
+ name: "response",
908
+ parts: [{ kind: "text", text }]
909
+ };
910
+ }
911
+
912
+ // src/a2a/handler.ts
913
+ var TERMINAL_STATES = /* @__PURE__ */ new Set([
914
+ "completed",
915
+ "canceled",
916
+ "failed",
917
+ "rejected"
918
+ ]);
919
+ var CancelRegistry = class {
920
+ controllers = /* @__PURE__ */ new Map();
921
+ register(taskId) {
922
+ const c = new AbortController();
923
+ this.controllers.set(taskId, c);
924
+ return c;
925
+ }
926
+ clear(taskId) {
927
+ this.controllers.delete(taskId);
928
+ }
929
+ cancel(taskId) {
930
+ const c = this.controllers.get(taskId);
931
+ if (!c) return false;
932
+ c.abort();
933
+ this.controllers.delete(taskId);
934
+ return true;
935
+ }
936
+ };
937
+ function createA2AHandlers(deps) {
938
+ const cancels = new CancelRegistry();
939
+ const handleAgentCard = async (c) => {
940
+ const slug = c.req.param("slug");
941
+ if (!slug) return c.json({ error: "slug required" }, 400);
942
+ const agent = await deps.config.resolveAgent(slug);
943
+ if (!agent) {
944
+ return c.json({ error: "Agent not found or not published" }, 404);
945
+ }
946
+ const url = new URL(c.req.url);
947
+ const agentUrl = `${url.origin}${url.pathname.replace(/\/\.well-known\/agent\.json$/, "")}`;
948
+ return c.json(buildAgentCard(agent, deps.config, agentUrl));
949
+ };
950
+ const handleJsonRpc = async (c) => {
951
+ const slug = c.req.param("slug");
952
+ if (!slug) {
953
+ return c.json(fail(null, A2A_ERROR_CODES.INVALID_REQUEST, "slug required"), 400);
954
+ }
955
+ const contentLength = Number.parseInt(c.req.header("Content-Length") ?? "0", 10);
956
+ if (contentLength > 65536) {
957
+ return c.json(fail(null, A2A_ERROR_CODES.INVALID_REQUEST, "request body too large (max 64KB)"), 413);
958
+ }
959
+ let raw;
960
+ try {
961
+ raw = await c.req.json();
962
+ } catch {
963
+ return c.json(fail(null, A2A_ERROR_CODES.PARSE_ERROR, "invalid JSON"), 400);
964
+ }
965
+ const parsed = parseEnvelope(raw);
966
+ if ("code" in parsed) {
967
+ return c.json(fail(parsed.id, parsed.code, parsed.message), 400);
968
+ }
969
+ switch (parsed.method) {
970
+ case "message/send":
971
+ return handleMessageSend(c, slug, parsed, deps);
972
+ case "message/stream":
973
+ return handleMessageStream(c, slug, parsed, deps, cancels);
974
+ case "tasks/get":
975
+ return handleTasksGet(c, parsed, deps);
976
+ case "tasks/cancel":
977
+ return handleTasksCancel(c, parsed, deps, cancels);
978
+ case "tasks/resubscribe":
979
+ return handleTasksResubscribe(c, parsed, deps);
980
+ case "tasks/pushNotificationConfig/set":
981
+ return handlePushSet(c, parsed, deps);
982
+ case "tasks/pushNotificationConfig/get":
983
+ return handlePushGet(c, parsed, deps);
984
+ case "tasks/pushNotificationConfig/list":
985
+ return handlePushList(c, parsed, deps);
986
+ case "tasks/pushNotificationConfig/delete":
987
+ return handlePushDelete(c, parsed, deps);
988
+ default:
989
+ return c.json(
990
+ fail(parsed.id, A2A_ERROR_CODES.METHOD_NOT_FOUND, `unknown method '${parsed.method}'`)
991
+ );
992
+ }
993
+ };
994
+ return { handleAgentCard, handleJsonRpc };
995
+ }
996
+ async function handleMessageSend(c, slug, req, deps) {
997
+ const guard = await guardMessageRequest(c, slug, req, deps);
998
+ if (guard instanceof Response) return guard;
999
+ const { authz, task } = guard;
1000
+ let responseText = "";
1001
+ let outputTokens = 0;
1002
+ let inputRequiredPrompt;
1003
+ let inputRequiredSeen = false;
1004
+ try {
1005
+ for await (const event of dispatchSandboxStreamRich(
1006
+ authz.agent,
1007
+ authz.userMessage,
1008
+ authz.consumerId,
1009
+ deps.config,
1010
+ void 0,
1011
+ task.id
1012
+ )) {
1013
+ if (event.kind === "text") {
1014
+ responseText += event.delta;
1015
+ outputTokens += estimateTokens(event.delta);
1016
+ } else {
1017
+ inputRequiredSeen = true;
1018
+ inputRequiredPrompt = event.prompt;
1019
+ }
1020
+ }
1021
+ } catch (err) {
1022
+ const failed = withStatus(task, "failed");
1023
+ await deps.taskStore.put(failed);
1024
+ await maybeDeliverPush(failed, deps);
1025
+ return c.json(
1026
+ fail(
1027
+ req.id,
1028
+ A2A_ERROR_CODES.INTERNAL_ERROR,
1029
+ err instanceof Error ? err.message : String(err)
1030
+ )
1031
+ );
1032
+ }
1033
+ await settleAndRecord(
1034
+ authz.agent,
1035
+ authz,
1036
+ estimateTokens(authz.userMessage),
1037
+ outputTokens,
1038
+ deps.config,
1039
+ deps.state.obs
1040
+ );
1041
+ if (inputRequiredSeen) {
1042
+ const paused = withStatus(
1043
+ task,
1044
+ "input-required",
1045
+ inputRequiredPrompt ? agentMessage(task, inputRequiredPrompt) : void 0,
1046
+ responseText ? [responseTextToArtifact(responseText, `${task.id}-artifact-0`)] : task.artifacts
1047
+ );
1048
+ await deps.taskStore.put(paused);
1049
+ return c.json(ok(req.id, paused));
1050
+ }
1051
+ const completed = withStatus(task, "completed", void 0, [
1052
+ responseTextToArtifact(responseText, `${task.id}-artifact-0`)
1053
+ ]);
1054
+ await deps.taskStore.put(completed);
1055
+ await maybeDeliverPush(completed, deps);
1056
+ return c.json(ok(req.id, completed));
1057
+ }
1058
+ async function handleMessageStream(c, slug, req, deps, cancels) {
1059
+ const guard = await guardMessageRequest(c, slug, req, deps);
1060
+ if (guard instanceof Response) return guard;
1061
+ const { authz, task } = guard;
1062
+ const controller = cancels.register(task.id);
1063
+ const inputTokens = estimateTokens(authz.userMessage);
1064
+ let outputTokens = 0;
1065
+ let responseText = "";
1066
+ const stream = new ReadableStream({
1067
+ async start(ctrl) {
1068
+ const encoder = new TextEncoder();
1069
+ const send = (event) => {
1070
+ ctrl.enqueue(encoder.encode(`data: ${JSON.stringify(ok(req.id, event))}
1071
+
1072
+ `));
1073
+ };
1074
+ const workingStatus = {
1075
+ kind: "status-update",
1076
+ taskId: task.id,
1077
+ contextId: task.contextId,
1078
+ status: { state: "working", timestamp: nowIso() },
1079
+ final: false
1080
+ };
1081
+ await deps.taskStore.put({ ...task, status: workingStatus.status });
1082
+ send(workingStatus);
1083
+ let inputRequiredPrompt;
1084
+ let inputRequiredSeen = false;
1085
+ try {
1086
+ for await (const event of dispatchSandboxStreamRich(
1087
+ authz.agent,
1088
+ authz.userMessage,
1089
+ authz.consumerId,
1090
+ deps.config,
1091
+ controller.signal,
1092
+ task.id
1093
+ )) {
1094
+ if (event.kind === "text") {
1095
+ responseText += event.delta;
1096
+ outputTokens += estimateTokens(event.delta);
1097
+ const artifactEvent = {
1098
+ kind: "artifact-update",
1099
+ taskId: task.id,
1100
+ contextId: task.contextId,
1101
+ artifact: {
1102
+ artifactId: `${task.id}-artifact-0`,
1103
+ name: "response",
1104
+ parts: [{ kind: "text", text: event.delta }]
1105
+ },
1106
+ append: true
1107
+ };
1108
+ send(artifactEvent);
1109
+ } else {
1110
+ inputRequiredSeen = true;
1111
+ inputRequiredPrompt = event.prompt;
1112
+ }
1113
+ }
1114
+ if (controller.signal.aborted) {
1115
+ const canceled = withStatus(task, "canceled", void 0, [
1116
+ responseTextToArtifact(responseText, `${task.id}-artifact-0`)
1117
+ ]);
1118
+ await deps.taskStore.put(canceled);
1119
+ send({
1120
+ kind: "status-update",
1121
+ taskId: task.id,
1122
+ contextId: task.contextId,
1123
+ status: canceled.status,
1124
+ final: true
1125
+ });
1126
+ await maybeDeliverPush(canceled, deps);
1127
+ return;
1128
+ }
1129
+ await settleAndRecord(
1130
+ authz.agent,
1131
+ authz,
1132
+ inputTokens,
1133
+ outputTokens,
1134
+ deps.config,
1135
+ deps.state.obs
1136
+ );
1137
+ if (inputRequiredSeen) {
1138
+ const paused = withStatus(
1139
+ task,
1140
+ "input-required",
1141
+ inputRequiredPrompt ? agentMessage(task, inputRequiredPrompt) : void 0,
1142
+ responseText ? [responseTextToArtifact(responseText, `${task.id}-artifact-0`)] : task.artifacts
1143
+ );
1144
+ await deps.taskStore.put(paused);
1145
+ send({
1146
+ kind: "status-update",
1147
+ taskId: task.id,
1148
+ contextId: task.contextId,
1149
+ status: paused.status,
1150
+ final: true
1151
+ });
1152
+ return;
1153
+ }
1154
+ send({
1155
+ kind: "artifact-update",
1156
+ taskId: task.id,
1157
+ contextId: task.contextId,
1158
+ artifact: {
1159
+ artifactId: `${task.id}-artifact-0`,
1160
+ name: "response",
1161
+ parts: [{ kind: "text", text: "" }]
1162
+ },
1163
+ append: true,
1164
+ lastChunk: true
1165
+ });
1166
+ const completed = withStatus(task, "completed", void 0, [
1167
+ responseTextToArtifact(responseText, `${task.id}-artifact-0`)
1168
+ ]);
1169
+ await deps.taskStore.put(completed);
1170
+ send({
1171
+ kind: "status-update",
1172
+ taskId: task.id,
1173
+ contextId: task.contextId,
1174
+ status: completed.status,
1175
+ final: true
1176
+ });
1177
+ await maybeDeliverPush(completed, deps);
1178
+ } catch (err) {
1179
+ const failed = withStatus(task, "failed");
1180
+ await deps.taskStore.put(failed);
1181
+ send({
1182
+ kind: "status-update",
1183
+ taskId: task.id,
1184
+ contextId: task.contextId,
1185
+ status: failed.status,
1186
+ final: true
1187
+ });
1188
+ await maybeDeliverPush(failed, deps);
1189
+ await deps.state.obs?.onStreamError?.(
1190
+ {
1191
+ requestId: authz.requestId,
1192
+ agentSlug: authz.agent.slug,
1193
+ startMs: authz.startMs
1194
+ },
1195
+ {
1196
+ consumerId: authz.consumerId,
1197
+ errorMessage: err instanceof Error ? err.message : String(err)
1198
+ }
1199
+ );
1200
+ } finally {
1201
+ cancels.clear(task.id);
1202
+ ctrl.close();
1203
+ }
1204
+ }
1205
+ });
1206
+ return new Response(stream, {
1207
+ headers: {
1208
+ "Content-Type": "text/event-stream",
1209
+ "Cache-Control": "no-cache",
1210
+ "X-Request-Id": authz.requestId,
1211
+ "X-Agent-Slug": authz.agent.slug,
1212
+ "X-Task-Id": task.id
1213
+ }
1214
+ });
1215
+ }
1216
+ async function handleTasksGet(c, req, deps) {
1217
+ const params = req.params;
1218
+ if (!params || typeof params.id !== "string") {
1219
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
1220
+ }
1221
+ const task = await deps.taskStore.get(params.id);
1222
+ if (!task) {
1223
+ return c.json(
1224
+ fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`)
1225
+ );
1226
+ }
1227
+ return c.json(ok(req.id, task));
1228
+ }
1229
+ async function handleTasksCancel(c, req, deps, cancels) {
1230
+ const params = req.params;
1231
+ if (!params || typeof params.id !== "string") {
1232
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
1233
+ }
1234
+ const task = await deps.taskStore.get(params.id);
1235
+ if (!task) {
1236
+ return c.json(
1237
+ fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`)
1238
+ );
1239
+ }
1240
+ if (isTerminal(task.status.state)) {
1241
+ return c.json(
1242
+ fail(
1243
+ req.id,
1244
+ A2A_ERROR_CODES.TASK_NOT_CANCELABLE,
1245
+ `task '${params.id}' is in terminal state '${task.status.state}'`
1246
+ )
1247
+ );
1248
+ }
1249
+ const stillActive = cancels.cancel(task.id);
1250
+ const canceled = {
1251
+ ...task,
1252
+ status: { state: "canceled", timestamp: nowIso() }
1253
+ };
1254
+ await deps.taskStore.put(canceled);
1255
+ if (!stillActive) {
1256
+ await maybeDeliverPush(canceled, deps);
1257
+ }
1258
+ return c.json(ok(req.id, canceled));
1259
+ }
1260
+ async function handleTasksResubscribe(c, req, deps) {
1261
+ const params = req.params;
1262
+ if (!params || typeof params.id !== "string") {
1263
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
1264
+ }
1265
+ const task = await deps.taskStore.get(params.id);
1266
+ if (!task) {
1267
+ return c.json(
1268
+ fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`)
1269
+ );
1270
+ }
1271
+ const final = isTerminal(task.status.state) || task.status.state === "input-required";
1272
+ const event = {
1273
+ kind: "status-update",
1274
+ taskId: task.id,
1275
+ contextId: task.contextId,
1276
+ status: task.status,
1277
+ final
1278
+ };
1279
+ const encoder = new TextEncoder();
1280
+ const stream = new ReadableStream({
1281
+ start(ctrl) {
1282
+ ctrl.enqueue(encoder.encode(`data: ${JSON.stringify(ok(req.id, event))}
1283
+
1284
+ `));
1285
+ ctrl.close();
1286
+ }
1287
+ });
1288
+ return new Response(stream, {
1289
+ headers: {
1290
+ "Content-Type": "text/event-stream",
1291
+ "Cache-Control": "no-cache",
1292
+ "X-Task-Id": task.id
1293
+ }
1294
+ });
1295
+ }
1296
+ async function handlePushSet(c, req, deps) {
1297
+ if (!deps.pushStore) {
1298
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
1299
+ }
1300
+ const params = req.params;
1301
+ if (!params || typeof params.taskId !== "string" || !params.pushNotificationConfig?.id) {
1302
+ return c.json(
1303
+ fail(
1304
+ req.id,
1305
+ A2A_ERROR_CODES.INVALID_PARAMS,
1306
+ "params.taskId and params.pushNotificationConfig.id required"
1307
+ )
1308
+ );
1309
+ }
1310
+ if (typeof params.pushNotificationConfig.url !== "string") {
1311
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "pushNotificationConfig.url required"));
1312
+ }
1313
+ const task = await deps.taskStore.get(params.taskId);
1314
+ if (!task) {
1315
+ return c.json(fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.taskId}' not found`));
1316
+ }
1317
+ await deps.pushStore.set(params.taskId, params.pushNotificationConfig);
1318
+ const stored = await deps.pushStore.get(params.taskId, params.pushNotificationConfig.id);
1319
+ return c.json(ok(req.id, { taskId: params.taskId, pushNotificationConfig: stored }));
1320
+ }
1321
+ async function handlePushGet(c, req, deps) {
1322
+ if (!deps.pushStore) {
1323
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
1324
+ }
1325
+ const params = req.params;
1326
+ if (!params || typeof params.id !== "string" || typeof params.pushNotificationConfigId !== "string") {
1327
+ return c.json(
1328
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id and params.pushNotificationConfigId required")
1329
+ );
1330
+ }
1331
+ const cfg = await deps.pushStore.get(params.id, params.pushNotificationConfigId);
1332
+ if (!cfg) {
1333
+ return c.json(
1334
+ fail(
1335
+ req.id,
1336
+ A2A_ERROR_CODES.TASK_NOT_FOUND,
1337
+ `push config '${params.pushNotificationConfigId}' not found for task '${params.id}'`
1338
+ )
1339
+ );
1340
+ }
1341
+ return c.json(ok(req.id, { taskId: params.id, pushNotificationConfig: cfg }));
1342
+ }
1343
+ async function handlePushList(c, req, deps) {
1344
+ if (!deps.pushStore) {
1345
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
1346
+ }
1347
+ const params = req.params;
1348
+ if (!params || typeof params.id !== "string") {
1349
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
1350
+ }
1351
+ const configs = await deps.pushStore.list(params.id);
1352
+ return c.json(ok(req.id, configs.map((cfg) => ({ taskId: params.id, pushNotificationConfig: cfg }))));
1353
+ }
1354
+ async function handlePushDelete(c, req, deps) {
1355
+ if (!deps.pushStore) {
1356
+ return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
1357
+ }
1358
+ const params = req.params;
1359
+ if (!params || typeof params.id !== "string" || typeof params.pushNotificationConfigId !== "string") {
1360
+ return c.json(
1361
+ fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id and params.pushNotificationConfigId required")
1362
+ );
1363
+ }
1364
+ await deps.pushStore.delete(params.id, params.pushNotificationConfigId);
1365
+ return c.json(ok(req.id, null));
1366
+ }
1367
+ async function guardMessageRequest(c, slug, req, deps) {
1368
+ const params = req.params;
1369
+ if (!params || !params.message) {
1370
+ return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.message required"));
1371
+ }
1372
+ const extracted = extractTextFromMessage(params.message);
1373
+ if ("error" in extracted) {
1374
+ return c.json(fail(req.id, extracted.error.code, extracted.error.message));
1375
+ }
1376
+ const guard = await authenticateAndGuard(
1377
+ c,
1378
+ slug,
1379
+ [{ role: "user", content: extracted.text }],
1380
+ deps.config,
1381
+ deps.state
1382
+ );
1383
+ if (guard instanceof Response) return guard;
1384
+ const authz = guard;
1385
+ if (typeof params.message.taskId === "string") {
1386
+ const existing = await deps.taskStore.get(params.message.taskId);
1387
+ if (existing) {
1388
+ if (existing.status.state !== "input-required") {
1389
+ return c.json(
1390
+ fail(
1391
+ req.id,
1392
+ A2A_ERROR_CODES.INVALID_PARAMS,
1393
+ `task '${existing.id}' is in state '${existing.status.state}'; only 'input-required' tasks accept follow-up messages`
1394
+ )
1395
+ );
1396
+ }
1397
+ const appendedMessage = {
1398
+ ...params.message,
1399
+ taskId: existing.id,
1400
+ contextId: existing.contextId
1401
+ };
1402
+ const continued = {
1403
+ ...existing,
1404
+ status: { state: "working", timestamp: nowIso() },
1405
+ history: [...existing.history ?? [], appendedMessage]
1406
+ };
1407
+ await deps.taskStore.put(continued);
1408
+ return { authz, task: continued };
1409
+ }
1410
+ }
1411
+ const taskId = params.message.taskId ?? `task_${cryptoRandomId()}`;
1412
+ const contextId = params.message.contextId ?? `ctx_${cryptoRandomId()}`;
1413
+ const initialMessage = {
1414
+ ...params.message,
1415
+ taskId,
1416
+ contextId
1417
+ };
1418
+ const task = {
1419
+ kind: "task",
1420
+ id: taskId,
1421
+ contextId,
1422
+ status: { state: "submitted", timestamp: nowIso() },
1423
+ history: [initialMessage]
1424
+ };
1425
+ await deps.taskStore.put(task);
1426
+ return { authz, task };
1427
+ }
1428
+ function isTerminal(state) {
1429
+ return state === "completed" || state === "canceled" || state === "failed" || state === "rejected";
1430
+ }
1431
+ function nowIso() {
1432
+ return (/* @__PURE__ */ new Date()).toISOString();
1433
+ }
1434
+ function cryptoRandomId() {
1435
+ return crypto.randomUUID().replace(/-/g, "");
1436
+ }
1437
+ function withStatus(task, state, message, artifacts) {
1438
+ return {
1439
+ ...task,
1440
+ status: { state, timestamp: nowIso(), ...message ? { message } : {} },
1441
+ ...artifacts !== void 0 ? { artifacts } : {}
1442
+ };
1443
+ }
1444
+ function agentMessage(task, text) {
1445
+ return {
1446
+ kind: "message",
1447
+ role: "agent",
1448
+ parts: [{ kind: "text", text }],
1449
+ messageId: `${task.id}-status-${task.status.state}-${nowIso()}`,
1450
+ taskId: task.id,
1451
+ contextId: task.contextId
1452
+ };
1453
+ }
1454
+ async function maybeDeliverPush(task, deps) {
1455
+ if (!deps.pushStore || !TERMINAL_STATES.has(task.status.state)) return;
1456
+ try {
1457
+ await deliverPushNotifications({
1458
+ task,
1459
+ store: deps.pushStore,
1460
+ webhookSecret: deps.config.a2a?.webhookSecret,
1461
+ fetcher: deps.config.a2a?.pushFetcher,
1462
+ onDelivery: (result) => {
1463
+ if (!result.ok) {
1464
+ deps.state.obs?.onStreamError?.(
1465
+ { requestId: result.taskId, agentSlug: task.id, startMs: Date.now() },
1466
+ {
1467
+ consumerId: result.configId,
1468
+ errorMessage: `push delivery failed (${result.status ?? "no-status"}): ${result.error ?? "non-2xx"}`
1469
+ }
1470
+ );
1471
+ }
1472
+ }
1473
+ });
1474
+ } catch (err) {
1475
+ console.error(
1476
+ `[agent-gateway] push delivery threw for task ${task.id}: ${err instanceof Error ? err.message : String(err)}`
1477
+ );
1478
+ }
1479
+ }
1480
+
1481
+ // src/a2a/task-store.ts
1482
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
1483
+ var InMemoryTaskStore = class {
1484
+ constructor(ttlMs = DEFAULT_TTL_MS) {
1485
+ this.ttlMs = ttlMs;
1486
+ }
1487
+ ttlMs;
1488
+ entries = /* @__PURE__ */ new Map();
1489
+ async get(id) {
1490
+ this.gc();
1491
+ const entry = this.entries.get(id);
1492
+ if (!entry) return void 0;
1493
+ return clone(entry.task);
1494
+ }
1495
+ async put(task) {
1496
+ this.gc();
1497
+ this.entries.set(task.id, { task: clone(task), expiresAt: Date.now() + this.ttlMs });
1498
+ }
1499
+ async delete(id) {
1500
+ this.entries.delete(id);
1501
+ }
1502
+ /**
1503
+ * Sweep expired tasks. Called inline on every read/write — cheap for the
1504
+ * Map sizes this is designed for (10s–1000s of concurrent tasks).
1505
+ */
1506
+ gc() {
1507
+ const now = Date.now();
1508
+ for (const [id, entry] of this.entries) {
1509
+ if (entry.expiresAt <= now) this.entries.delete(id);
1510
+ }
1511
+ }
1512
+ };
1513
+ function clone(value) {
1514
+ return JSON.parse(JSON.stringify(value));
1515
+ }
1516
+
1517
+ // src/middleware.ts
1518
+ function createAgentGateway(config) {
1519
+ if (!config.x402.verifySigner && !config.x402.demoMode) {
1520
+ throw new Error(
1521
+ "createAgentGateway: x402.verifySigner is required in production. For tests, set x402.demoMode: true explicitly."
1522
+ );
1523
+ }
1524
+ const gw = new Hono();
1525
+ const rateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore();
1526
+ const state = {
1527
+ rateLimitStore,
1528
+ nonceStore: config.nonceStore ?? new MemoryNonceStore(),
1529
+ globalRateLimit: config.rateLimit ?? { limit: 60, windowSeconds: 60 },
1530
+ requiredScope: config.requiredScope ?? "chat",
1531
+ maxLen: config.maxMessageLength ?? 8e3,
1532
+ obs: config.observer
1533
+ };
1534
+ const obs = state.obs;
1535
+ gw.get("/:slug/chat/completions", async (c) => {
1536
+ const slug = c.req.param("slug");
1537
+ const agent = await config.resolveAgent(slug);
1538
+ if (!agent) return c.json({ error: "Agent not found or not published" }, 404);
1539
+ const paymentMethods = [
1540
+ {
1541
+ type: "x402",
1542
+ operator: config.x402.operatorAddress,
1543
+ chain_id: config.x402.chainId,
1544
+ credits_contract: config.x402.creditsAddress
1545
+ }
1546
+ ];
1547
+ if (config.mpp) {
1548
+ paymentMethods.push({
1549
+ type: "mpp",
1550
+ realm: config.mpp.realm,
1551
+ method: config.mpp.method ?? "blueprintevm"
1552
+ });
1553
+ }
1554
+ paymentMethods.push({ type: "api_key", prefix: "sk_agent_" });
1555
+ return c.json({
1556
+ slug: agent.slug,
1557
+ pricing: {
1558
+ per_token_usd: agent.pricePerTokenUsd,
1559
+ currency: "USD",
1560
+ platform_fee_percent: agent.platformFeePercent
1561
+ },
1562
+ hosting: {
1563
+ mode: agent.sandboxEndpoint ? "sovereign" : "centralized",
1564
+ endpoint: agent.sandboxEndpoint ?? config.baseUrl ?? "tangle.tools"
1565
+ },
1566
+ payment_methods: paymentMethods,
1567
+ capabilities: ["chat.completions", "streaming"],
1568
+ openai_compatible: true
1569
+ });
1570
+ });
1571
+ gw.post("/:slug/chat/completions", async (c) => {
1572
+ const slug = c.req.param("slug");
1573
+ const contentLength = Number.parseInt(c.req.header("Content-Length") ?? "0", 10);
1574
+ if (contentLength > 65536) {
1575
+ const requestId = generateRequestId();
1576
+ await obs?.onBodyTooLarge?.(
1577
+ { requestId, agentSlug: slug, startMs: Date.now() },
1578
+ contentLength
1579
+ );
1580
+ return c.json(
1581
+ {
1582
+ error: {
1583
+ message: "Request body too large (max 64KB)",
1584
+ type: "invalid_request"
1585
+ }
1586
+ },
1587
+ 413
1588
+ );
1589
+ }
1590
+ let body;
1591
+ try {
1592
+ body = await c.req.json();
1593
+ } catch {
1594
+ return c.json({ error: { message: "Invalid JSON", type: "invalid_request" } }, 400);
1595
+ }
1596
+ if (!body.messages?.length) {
1597
+ return c.json(
1598
+ { error: { message: "messages array required", type: "invalid_request" } },
1599
+ 400
1600
+ );
1601
+ }
1602
+ const guard = await authenticateAndGuard(c, slug, body.messages, config, state);
1603
+ if (guard instanceof Response) return guard;
1604
+ const authz = guard;
1605
+ return streamChatCompletions(c, authz, config, obs);
1606
+ });
1607
+ const taskStore = config.a2a?.taskStore ?? new InMemoryTaskStore();
1608
+ const pushStore = config.a2a?.pushStore;
1609
+ const a2a = createA2AHandlers({ config, state, taskStore, pushStore });
1610
+ gw.get("/:slug/.well-known/agent.json", a2a.handleAgentCard);
1611
+ gw.post("/:slug", a2a.handleJsonRpc);
1612
+ return gw;
1613
+ }
1614
+ function streamChatCompletions(c, authz, config, obs) {
1615
+ const { agent, consumerId, paymentMethod, requestId, userMessage, rateLimitRemaining } = authz;
1616
+ const inputTokens = estimateTokens(userMessage);
1617
+ let outputTokens = 0;
1618
+ const ctx = {
1619
+ requestId,
1620
+ agentSlug: agent.slug,
1621
+ startMs: authz.startMs
1622
+ };
1623
+ const stream = new ReadableStream({
1624
+ async start(controller) {
1625
+ const encoder = new TextEncoder();
1626
+ const sendChunk = (delta) => {
1627
+ outputTokens += estimateTokens(delta);
1628
+ const chunk = {
1629
+ id: `chatcmpl-${Date.now()}`,
1630
+ object: "chat.completion.chunk",
1631
+ created: Math.floor(Date.now() / 1e3),
1632
+ model: agent.slug,
1633
+ choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
1634
+ };
1635
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}
1636
+
1637
+ `));
1638
+ };
1639
+ try {
1640
+ for await (const delta of dispatchSandboxStream(agent, userMessage, consumerId, config)) {
1641
+ sendChunk(delta);
1642
+ }
1643
+ const done = {
1644
+ id: `chatcmpl-${Date.now()}`,
1645
+ object: "chat.completion.chunk",
1646
+ created: Math.floor(Date.now() / 1e3),
1647
+ model: agent.slug,
1648
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
1649
+ };
1650
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}
1651
+
1652
+ `));
1653
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
1654
+ await settleAndRecord(agent, authz, inputTokens, outputTokens, config, obs);
1655
+ } catch (err) {
1656
+ const rawMessage = err instanceof Error ? err.message : String(err);
1657
+ const safeMessage = rawMessage.includes("/") || rawMessage.includes("\\") ? "Internal agent error" : rawMessage;
1658
+ await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage });
1659
+ controller.enqueue(
1660
+ encoder.encode(
1661
+ `data: ${JSON.stringify({ error: { message: safeMessage, type: "server_error" } })}
1662
+
1663
+ `
1664
+ )
1665
+ );
1666
+ } finally {
1667
+ controller.close();
1668
+ }
1669
+ }
1670
+ });
1671
+ return new Response(stream, {
1672
+ headers: {
1673
+ "Content-Type": "text/event-stream",
1674
+ "Cache-Control": "no-cache",
1675
+ "X-Request-Id": requestId,
1676
+ "X-Agent-Slug": agent.slug,
1677
+ "X-Agent-Hosting": agent.sandboxEndpoint ? "sovereign" : "centralized",
1678
+ "X-Payment-Method": paymentMethod,
1679
+ "X-Payment-Settled": paymentMethod === "x402" ? "pending" : "true",
1680
+ ...rateLimitRemaining !== void 0 ? { "X-RateLimit-Remaining": String(rateLimitRemaining) } : {}
1681
+ }
1682
+ });
1683
+ }
1684
+
1685
+ export {
1686
+ detectInjection,
1687
+ filterConsumerMessages,
1688
+ filterConsumerMessagesStrict,
1689
+ redactSystemPromptFromOutput,
1690
+ ConsoleObserver,
1691
+ CompositeObserver,
1692
+ generateRequestId,
1693
+ verifyX402,
1694
+ verifyMpp,
1695
+ defaultVerifyApiKey,
1696
+ A2A_ERROR_CODES,
1697
+ InMemoryPushNotificationStore,
1698
+ SqlPushNotificationStore,
1699
+ deliverPushNotifications,
1700
+ InMemoryTaskStore,
1701
+ createAgentGateway
1702
+ };
1703
+ //# sourceMappingURL=chunk-3IKQWFKX.js.map