@tangle-network/agent-gateway 0.6.0 → 0.7.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 +31 -6
- package/dist/chunk-Q4YAIEZY.js +1763 -0
- package/dist/chunk-Q4YAIEZY.js.map +1 -0
- package/dist/index.d.ts +13 -9
- package/dist/index.js +112 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.js +1 -1
- package/dist/types-DEsMmS-X.d.ts +875 -0
- package/dist/types.d.ts +1 -1
- package/package.json +14 -10
- package/src/a2a/agent-card.ts +55 -0
- package/src/a2a/handler.ts +797 -0
- package/src/a2a/jsonrpc.ts +65 -0
- package/src/a2a/push-notifications.ts +299 -0
- package/src/a2a/task-store-sql.ts +189 -0
- package/src/a2a/task-store.ts +53 -0
- package/src/a2a/translate.ts +77 -0
- package/src/a2a/types.ts +217 -0
- package/src/dispatch.ts +486 -0
- package/src/index.ts +58 -1
- package/src/middleware.ts +139 -294
- package/src/types.ts +76 -2
- package/src/verify.ts +93 -26
- package/dist/chunk-373QHRKV.js +0 -635
- package/dist/chunk-373QHRKV.js.map +0 -1
- package/dist/types-C_L7yXXI.d.ts +0 -362
|
@@ -0,0 +1,1763 @@
|
|
|
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
|
+
function isApiKeyAuthEnabled(config) {
|
|
264
|
+
return config.verifyApiKey !== void 0 || config.x402.demoMode === true;
|
|
265
|
+
}
|
|
266
|
+
function isMppAuthEnabled(config) {
|
|
267
|
+
const method = config.mpp?.method ?? "blueprintevm";
|
|
268
|
+
return Boolean(
|
|
269
|
+
config.mpp && (config.mpp.verifySigner !== void 0 || method === "blueprintevm" && config.x402.verifySigner !== void 0 || config.x402.demoMode === true)
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
async function verifyX402(spendAuthHeader, config, nonceStore) {
|
|
273
|
+
try {
|
|
274
|
+
const raw = JSON.parse(spendAuthHeader);
|
|
275
|
+
if (!raw.commitment || !raw.signature || !raw.amount) return null;
|
|
276
|
+
if (raw.operator?.toLowerCase() !== config.operatorAddress.toLowerCase()) return null;
|
|
277
|
+
const amount = BigInt(raw.amount);
|
|
278
|
+
const nonce = BigInt(raw.nonce);
|
|
279
|
+
const expiry = BigInt(raw.expiry);
|
|
280
|
+
if (expiry < BigInt(Math.floor(Date.now() / 1e3))) return null;
|
|
281
|
+
if (amount <= 0n) return null;
|
|
282
|
+
const nonceKey = `${raw.commitment}:${nonce.toString()}`;
|
|
283
|
+
if (nonceStore && await nonceStore.hasSeen(nonceKey)) return null;
|
|
284
|
+
if (config.verifySigner) {
|
|
285
|
+
const verified = await config.verifySigner(raw);
|
|
286
|
+
if (!verified) return null;
|
|
287
|
+
} else if (!config.demoMode) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if (nonceStore) {
|
|
291
|
+
const ttl = Math.min(Number(expiry) - Math.floor(Date.now() / 1e3), 3600);
|
|
292
|
+
await nonceStore.markSeen(nonceKey, Math.max(ttl, 60));
|
|
293
|
+
}
|
|
294
|
+
return raw.commitment;
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function verifyMpp(authHeader, config, x402Config, nonceStore) {
|
|
300
|
+
const match = authHeader.match(/^Payment\s+(\S+)\s+(\S+)$/i);
|
|
301
|
+
if (!match) return null;
|
|
302
|
+
const [, method, credentialB64] = match;
|
|
303
|
+
if (config.method && method !== config.method) return null;
|
|
304
|
+
try {
|
|
305
|
+
if (!/^[A-Za-z0-9_-]+$/.test(credentialB64)) return null;
|
|
306
|
+
const decoded = Buffer.from(credentialB64, "base64url").toString("utf-8");
|
|
307
|
+
let payload = {};
|
|
308
|
+
try {
|
|
309
|
+
const credential = JSON.parse(decoded);
|
|
310
|
+
if (credential && typeof credential === "object" && !Array.isArray(credential)) {
|
|
311
|
+
const nested = credential.payload;
|
|
312
|
+
payload = nested && typeof nested === "object" && !Array.isArray(nested) ? nested : credential;
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
const nonceKey = nonceStore && payload.nonce !== void 0 ? `mpp:${method}:${String(payload.commitment ?? payload.from ?? "unknown")}:${String(payload.nonce)}` : null;
|
|
317
|
+
if (nonceKey && await nonceStore.hasSeen(nonceKey)) return null;
|
|
318
|
+
let consumerId = null;
|
|
319
|
+
if (config.verifySigner) {
|
|
320
|
+
consumerId = await config.verifySigner(payload, { method, credential: decoded });
|
|
321
|
+
} else if (method === "blueprintevm" && x402Config.verifySigner && payload.commitment) {
|
|
322
|
+
const verified = await x402Config.verifySigner(payload);
|
|
323
|
+
consumerId = verified ? String(payload.commitment) : null;
|
|
324
|
+
} else if (x402Config.demoMode) {
|
|
325
|
+
const identity = payload.commitment ?? payload.from;
|
|
326
|
+
if (typeof identity !== "string" || identity.length === 0) return null;
|
|
327
|
+
consumerId = identity;
|
|
328
|
+
} else {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (!consumerId) return null;
|
|
332
|
+
const operator = payload.operator ?? payload.to;
|
|
333
|
+
if (operator !== void 0) {
|
|
334
|
+
if (typeof operator !== "string" || operator.toLowerCase() !== x402Config.operatorAddress.toLowerCase()) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (payload.amount !== void 0 && BigInt(String(payload.amount)) <= 0n) return null;
|
|
339
|
+
if (payload.nonce !== void 0) BigInt(String(payload.nonce));
|
|
340
|
+
if (payload.expiry !== void 0 && BigInt(String(payload.expiry)) < BigInt(Math.floor(Date.now() / 1e3))) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
if (nonceStore && payload.nonce !== void 0) {
|
|
344
|
+
const expiry = payload.expiry === void 0 ? Math.floor(Date.now() / 1e3) + 3600 : Number(payload.expiry);
|
|
345
|
+
const ttl = Math.min(expiry - Math.floor(Date.now() / 1e3), 3600);
|
|
346
|
+
await nonceStore.markSeen(nonceKey, Math.max(ttl, 60));
|
|
347
|
+
}
|
|
348
|
+
return consumerId;
|
|
349
|
+
} catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function defaultVerifyApiKey(authHeader) {
|
|
354
|
+
if (!authHeader.startsWith("Bearer sk_agent_")) return null;
|
|
355
|
+
const key = authHeader.slice(7);
|
|
356
|
+
return {
|
|
357
|
+
keyId: key.slice(0, 16),
|
|
358
|
+
consumerId: `apikey:${key.slice(0, 16)}`
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/dispatch.ts
|
|
363
|
+
async function authenticateAndGuard(c, slug, messages, config, state) {
|
|
364
|
+
const startMs = Date.now();
|
|
365
|
+
const requestId = generateRequestId();
|
|
366
|
+
const ctx = { requestId, agentSlug: slug, startMs };
|
|
367
|
+
await state.obs?.onRequestStart?.(ctx);
|
|
368
|
+
const agent = await config.resolveAgent(slug);
|
|
369
|
+
if (!agent || !agent.enabled) {
|
|
370
|
+
return c.json({ error: { message: "Agent not found", type: "not_found" } }, 404);
|
|
371
|
+
}
|
|
372
|
+
if (!messages?.length) {
|
|
373
|
+
return c.json(
|
|
374
|
+
{ error: { message: "messages array required", type: "invalid_request" } },
|
|
375
|
+
400
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const spendAuthHeader = c.req.header("X-Payment-Signature");
|
|
379
|
+
const authHeader = c.req.header("Authorization") ?? "";
|
|
380
|
+
let consumerId = null;
|
|
381
|
+
let paymentMethod = "none";
|
|
382
|
+
let keyInfo = null;
|
|
383
|
+
if (spendAuthHeader) {
|
|
384
|
+
const signer = await verifyX402(spendAuthHeader, config.x402, state.nonceStore);
|
|
385
|
+
if (!signer) {
|
|
386
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
387
|
+
method: "x402",
|
|
388
|
+
code: "invalid_spend_auth",
|
|
389
|
+
httpStatus: 402
|
|
390
|
+
});
|
|
391
|
+
return c.json(
|
|
392
|
+
{
|
|
393
|
+
error: {
|
|
394
|
+
message: "Invalid X-Payment-Signature",
|
|
395
|
+
type: "authentication_error",
|
|
396
|
+
code: "invalid_spend_auth"
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
status: 402,
|
|
401
|
+
headers: { "X-Payment-Required": "spendauth", "X-Request-Id": requestId }
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
consumerId = signer;
|
|
406
|
+
paymentMethod = "x402";
|
|
407
|
+
} else if (isMppAuthEnabled(config) && authHeader.toLowerCase().startsWith("payment ")) {
|
|
408
|
+
const signer = await verifyMpp(authHeader, config.mpp, config.x402, state.nonceStore);
|
|
409
|
+
if (!signer) {
|
|
410
|
+
const realm = config.mpp.realm;
|
|
411
|
+
const method = config.mpp.method ?? "blueprintevm";
|
|
412
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
413
|
+
method: "mpp",
|
|
414
|
+
code: "invalid_mpp_credential",
|
|
415
|
+
httpStatus: 401
|
|
416
|
+
});
|
|
417
|
+
return c.json(
|
|
418
|
+
{
|
|
419
|
+
error: {
|
|
420
|
+
message: "Invalid Payment credential",
|
|
421
|
+
type: "authentication_error",
|
|
422
|
+
code: "invalid_mpp_credential"
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
status: 401,
|
|
427
|
+
headers: {
|
|
428
|
+
"WWW-Authenticate": `Payment realm="${realm}", method="${method}"`,
|
|
429
|
+
"X-Request-Id": requestId
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
consumerId = signer;
|
|
435
|
+
paymentMethod = "mpp";
|
|
436
|
+
} else if (authHeader.startsWith("Bearer ")) {
|
|
437
|
+
const verify = config.verifyApiKey ?? (config.x402.demoMode ? defaultVerifyApiKey : null);
|
|
438
|
+
if (!verify || !isApiKeyAuthEnabled(config)) {
|
|
439
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
440
|
+
method: "apikey",
|
|
441
|
+
code: "api_keys_not_configured",
|
|
442
|
+
httpStatus: 401
|
|
443
|
+
});
|
|
444
|
+
return c.json(
|
|
445
|
+
{ error: { message: "API key authentication is not configured", type: "authentication_error" } },
|
|
446
|
+
{ status: 401, headers: { "X-Request-Id": requestId } }
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const key = await verify(authHeader);
|
|
450
|
+
if (!key) {
|
|
451
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
452
|
+
method: "apikey",
|
|
453
|
+
code: "invalid_api_key",
|
|
454
|
+
httpStatus: 401
|
|
455
|
+
});
|
|
456
|
+
return c.json(
|
|
457
|
+
{ error: { message: "Invalid API key", type: "authentication_error" } },
|
|
458
|
+
{ status: 401, headers: { "X-Request-Id": requestId } }
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (key.scopes && key.scopes.length > 0 && !key.scopes.includes(state.requiredScope)) {
|
|
462
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
463
|
+
method: "apikey",
|
|
464
|
+
code: "insufficient_scope",
|
|
465
|
+
httpStatus: 403
|
|
466
|
+
});
|
|
467
|
+
return c.json(
|
|
468
|
+
{
|
|
469
|
+
error: {
|
|
470
|
+
message: `API key missing required scope: ${state.requiredScope}`,
|
|
471
|
+
type: "forbidden",
|
|
472
|
+
code: "insufficient_scope"
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
{ status: 403, headers: { "X-Request-Id": requestId } }
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
consumerId = key.consumerId;
|
|
479
|
+
paymentMethod = "apikey";
|
|
480
|
+
keyInfo = key;
|
|
481
|
+
} else {
|
|
482
|
+
await state.obs?.onAuthFailure?.(ctx, {
|
|
483
|
+
method: "none",
|
|
484
|
+
code: "payment_required",
|
|
485
|
+
httpStatus: 402
|
|
486
|
+
});
|
|
487
|
+
const methods = ["x402"];
|
|
488
|
+
if (isMppAuthEnabled(config)) methods.push("mpp");
|
|
489
|
+
if (isApiKeyAuthEnabled(config)) methods.push("api_key");
|
|
490
|
+
const headers = {
|
|
491
|
+
"X-Payment-Required": methods.join(", "),
|
|
492
|
+
"X-Request-Id": requestId
|
|
493
|
+
};
|
|
494
|
+
if (isMppAuthEnabled(config) && config.mpp) {
|
|
495
|
+
headers["WWW-Authenticate"] = `Payment realm="${config.mpp.realm}", method="${config.mpp.method ?? "blueprintevm"}"`;
|
|
496
|
+
}
|
|
497
|
+
return c.json(
|
|
498
|
+
{
|
|
499
|
+
error: {
|
|
500
|
+
message: "Payment required",
|
|
501
|
+
type: "payment_required",
|
|
502
|
+
payment_methods: methods,
|
|
503
|
+
x402: {
|
|
504
|
+
operator: config.x402.operatorAddress,
|
|
505
|
+
chain_id: config.x402.chainId,
|
|
506
|
+
credits_address: config.x402.creditsAddress,
|
|
507
|
+
estimated_amount_per_request: "20000"
|
|
508
|
+
},
|
|
509
|
+
...isMppAuthEnabled(config) && config.mpp ? { mpp: { realm: config.mpp.realm, method: config.mpp.method ?? "blueprintevm" } } : {},
|
|
510
|
+
...isApiKeyAuthEnabled(config) ? {
|
|
511
|
+
api_key: {
|
|
512
|
+
purchase_url: config.baseUrl ? `${config.baseUrl}/agents/${slug}/api-keys` : void 0
|
|
513
|
+
}
|
|
514
|
+
} : {}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
{ status: 402, headers }
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
await state.obs?.onPaymentVerified?.(ctx, {
|
|
521
|
+
method: paymentMethod,
|
|
522
|
+
consumerId,
|
|
523
|
+
keyId: keyInfo?.keyId
|
|
524
|
+
});
|
|
525
|
+
const effectiveRateLimit = keyInfo?.rateLimitPerMinute ? { limit: keyInfo.rateLimitPerMinute, windowSeconds: 60 } : state.globalRateLimit;
|
|
526
|
+
const rl = await checkRateLimit(consumerId, effectiveRateLimit, state.rateLimitStore);
|
|
527
|
+
if (!rl.allowed) {
|
|
528
|
+
await state.obs?.onRateLimited?.(ctx, {
|
|
529
|
+
consumerId,
|
|
530
|
+
retryAfterSeconds: rl.retryAfterSeconds ?? 60
|
|
531
|
+
});
|
|
532
|
+
return c.json(
|
|
533
|
+
{
|
|
534
|
+
error: {
|
|
535
|
+
message: "Rate limit exceeded",
|
|
536
|
+
type: "rate_limit_error",
|
|
537
|
+
retry_after: rl.retryAfterSeconds
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
status: 429,
|
|
542
|
+
headers: {
|
|
543
|
+
"Retry-After": String(rl.retryAfterSeconds ?? 60),
|
|
544
|
+
"X-Request-Id": requestId
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
const { messages: filtered, injectionWarnings } = filterConsumerMessagesStrict(
|
|
550
|
+
messages,
|
|
551
|
+
state.maxLen
|
|
552
|
+
);
|
|
553
|
+
if (injectionWarnings.length > 0) {
|
|
554
|
+
await state.obs?.onInjectionDetected?.(ctx, {
|
|
555
|
+
consumerId,
|
|
556
|
+
patterns: injectionWarnings,
|
|
557
|
+
blocked: !!config.blockInjection
|
|
558
|
+
});
|
|
559
|
+
if (config.blockInjection) {
|
|
560
|
+
return c.json(
|
|
561
|
+
{
|
|
562
|
+
error: {
|
|
563
|
+
message: "Request rejected: potential prompt injection detected",
|
|
564
|
+
type: "content_policy_violation"
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
{ status: 400, headers: { "X-Request-Id": requestId } }
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const userMessage = filtered.filter((m) => m.role === "user").map((m) => m.content).join("\n\n");
|
|
572
|
+
if (!userMessage) {
|
|
573
|
+
return c.json(
|
|
574
|
+
{ error: { message: "No user message provided", type: "invalid_request" } },
|
|
575
|
+
400
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
if (config.authorizeConsumer) {
|
|
579
|
+
const authz = await config.authorizeConsumer(agent, {
|
|
580
|
+
method: paymentMethod,
|
|
581
|
+
consumerId,
|
|
582
|
+
keyId: keyInfo?.keyId,
|
|
583
|
+
requestId
|
|
584
|
+
});
|
|
585
|
+
if (!authz.allow) {
|
|
586
|
+
return c.json(
|
|
587
|
+
{
|
|
588
|
+
error: {
|
|
589
|
+
message: authz.reason,
|
|
590
|
+
type: "authorization_denied",
|
|
591
|
+
code: authz.code
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
{ status: 403, headers: { "X-Request-Id": requestId } }
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
agent,
|
|
600
|
+
consumerId,
|
|
601
|
+
paymentMethod,
|
|
602
|
+
keyInfo,
|
|
603
|
+
userMessage,
|
|
604
|
+
rateLimitRemaining: rl.remaining,
|
|
605
|
+
requestId,
|
|
606
|
+
startMs
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async function* dispatchSandboxStream(agent, userMessage, consumerId, config, signal, sessionId) {
|
|
610
|
+
for await (const event of dispatchSandboxStreamRich(
|
|
611
|
+
agent,
|
|
612
|
+
userMessage,
|
|
613
|
+
consumerId,
|
|
614
|
+
config,
|
|
615
|
+
signal,
|
|
616
|
+
sessionId
|
|
617
|
+
)) {
|
|
618
|
+
if (event.kind === "text") yield event.delta;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
async function* dispatchSandboxStreamRich(agent, userMessage, consumerId, config, signal, sessionId) {
|
|
622
|
+
const box = await config.getSandbox(agent);
|
|
623
|
+
const promptStream = box.streamPrompt(userMessage, {
|
|
624
|
+
sessionId: sessionId ?? `consumer:${consumerId}`,
|
|
625
|
+
systemPrompt: agent.systemPrompt
|
|
626
|
+
});
|
|
627
|
+
for await (const event of promptStream) {
|
|
628
|
+
if (signal?.aborted) return;
|
|
629
|
+
if (event.type === "message.part.updated" && event.data?.part?.type === "text" && event.data.delta) {
|
|
630
|
+
yield {
|
|
631
|
+
kind: "text",
|
|
632
|
+
delta: redactSystemPromptFromOutput(event.data.delta, agent.systemPrompt)
|
|
633
|
+
};
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (event.type === "input-required" || event.data?.inputRequired) {
|
|
637
|
+
yield { kind: "input-required", prompt: event.data?.inputRequired?.prompt };
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function settleAndRecord(agent, authz, inputTokens, outputTokens, config, obs) {
|
|
643
|
+
const totalCost = (inputTokens + outputTokens) * agent.pricePerTokenUsd;
|
|
644
|
+
const ownerEarned = totalCost * (1 - agent.platformFeePercent);
|
|
645
|
+
const platformFee = totalCost * agent.platformFeePercent;
|
|
646
|
+
const usageEvent = {
|
|
647
|
+
requestId: authz.requestId,
|
|
648
|
+
agentId: agent.id,
|
|
649
|
+
agentSlug: agent.slug,
|
|
650
|
+
consumerId: authz.consumerId,
|
|
651
|
+
paymentMethod: authz.paymentMethod,
|
|
652
|
+
inputTokens,
|
|
653
|
+
outputTokens,
|
|
654
|
+
totalCostUsd: totalCost,
|
|
655
|
+
ownerEarnedUsd: ownerEarned,
|
|
656
|
+
platformFeeUsd: platformFee,
|
|
657
|
+
durationMs: Date.now() - authz.startMs
|
|
658
|
+
};
|
|
659
|
+
await config.recordUsage(usageEvent);
|
|
660
|
+
const ctx = {
|
|
661
|
+
requestId: authz.requestId,
|
|
662
|
+
agentSlug: agent.slug,
|
|
663
|
+
startMs: authz.startMs
|
|
664
|
+
};
|
|
665
|
+
await obs?.onRequestComplete?.(ctx, usageEvent);
|
|
666
|
+
if (config.settlePayment) {
|
|
667
|
+
await config.settlePayment(
|
|
668
|
+
{
|
|
669
|
+
method: authz.paymentMethod,
|
|
670
|
+
consumerId: authz.consumerId,
|
|
671
|
+
requestId: authz.requestId
|
|
672
|
+
},
|
|
673
|
+
totalCost
|
|
674
|
+
).catch(async (err) => {
|
|
675
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
676
|
+
console.error(`[agent-gateway] settlement failed for ${authz.consumerId}: ${msg}`);
|
|
677
|
+
await obs?.onSettlementError?.(ctx, {
|
|
678
|
+
consumerId: authz.consumerId,
|
|
679
|
+
method: authz.paymentMethod,
|
|
680
|
+
errorMessage: msg
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function estimateTokens(text) {
|
|
686
|
+
return Math.ceil(text.length / 4);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/a2a/agent-card.ts
|
|
690
|
+
function buildAgentCard(agent, config, agentUrl) {
|
|
691
|
+
const schemes = ["x402"];
|
|
692
|
+
if (isMppAuthEnabled(config)) schemes.push("mpp");
|
|
693
|
+
if (isApiKeyAuthEnabled(config)) schemes.push("Bearer");
|
|
694
|
+
const skills = agent.skills && agent.skills.length > 0 ? agent.skills : [
|
|
695
|
+
{
|
|
696
|
+
id: "chat",
|
|
697
|
+
name: agent.slug,
|
|
698
|
+
description: agent.description ?? `${agent.slug} agent`,
|
|
699
|
+
tags: ["chat"],
|
|
700
|
+
inputModes: ["text"],
|
|
701
|
+
outputModes: ["text"]
|
|
702
|
+
}
|
|
703
|
+
];
|
|
704
|
+
return {
|
|
705
|
+
name: agent.slug,
|
|
706
|
+
description: agent.description ?? `${agent.slug} agent`,
|
|
707
|
+
url: agentUrl,
|
|
708
|
+
version: "1.0.0",
|
|
709
|
+
provider: { organization: "Tangle", url: "https://tangle.tools" },
|
|
710
|
+
capabilities: {
|
|
711
|
+
streaming: true,
|
|
712
|
+
pushNotifications: !!config.a2a?.pushStore,
|
|
713
|
+
stateTransitionHistory: false
|
|
714
|
+
},
|
|
715
|
+
authentication: { schemes },
|
|
716
|
+
defaultInputModes: ["text"],
|
|
717
|
+
defaultOutputModes: ["text"],
|
|
718
|
+
skills
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/a2a/types.ts
|
|
723
|
+
var A2A_ERROR_CODES = {
|
|
724
|
+
PARSE_ERROR: -32700,
|
|
725
|
+
INVALID_REQUEST: -32600,
|
|
726
|
+
METHOD_NOT_FOUND: -32601,
|
|
727
|
+
INVALID_PARAMS: -32602,
|
|
728
|
+
INTERNAL_ERROR: -32603,
|
|
729
|
+
TASK_NOT_FOUND: -32001,
|
|
730
|
+
TASK_NOT_CANCELABLE: -32002,
|
|
731
|
+
PUSH_NOT_SUPPORTED: -32003,
|
|
732
|
+
UNSUPPORTED_OPERATION: -32004,
|
|
733
|
+
CONTENT_TYPE_NOT_SUPPORTED: -32005,
|
|
734
|
+
INVALID_AGENT_RESPONSE: -32006,
|
|
735
|
+
AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED: -32007
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
// src/a2a/jsonrpc.ts
|
|
739
|
+
function parseEnvelope(raw) {
|
|
740
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
741
|
+
return {
|
|
742
|
+
id: null,
|
|
743
|
+
code: A2A_ERROR_CODES.INVALID_REQUEST,
|
|
744
|
+
message: "request must be a JSON object"
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const req = raw;
|
|
748
|
+
const id = req.id === void 0 ? null : req.id;
|
|
749
|
+
if (req.jsonrpc !== "2.0") {
|
|
750
|
+
return { id, code: A2A_ERROR_CODES.INVALID_REQUEST, message: 'jsonrpc field must be "2.0"' };
|
|
751
|
+
}
|
|
752
|
+
if (typeof req.method !== "string" || req.method.length === 0) {
|
|
753
|
+
return { id, code: A2A_ERROR_CODES.INVALID_REQUEST, message: "method field required" };
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
jsonrpc: "2.0",
|
|
757
|
+
id,
|
|
758
|
+
method: req.method,
|
|
759
|
+
params: req.params
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function ok(id, result) {
|
|
763
|
+
return { jsonrpc: "2.0", id, result };
|
|
764
|
+
}
|
|
765
|
+
function fail(id, code, message, data) {
|
|
766
|
+
return {
|
|
767
|
+
jsonrpc: "2.0",
|
|
768
|
+
id,
|
|
769
|
+
error: { code, message, ...data !== void 0 ? { data } : {} }
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/a2a/push-notifications.ts
|
|
774
|
+
var InMemoryPushNotificationStore = class {
|
|
775
|
+
byTask = /* @__PURE__ */ new Map();
|
|
776
|
+
async set(taskId, config) {
|
|
777
|
+
let configs = this.byTask.get(taskId);
|
|
778
|
+
if (!configs) {
|
|
779
|
+
configs = /* @__PURE__ */ new Map();
|
|
780
|
+
this.byTask.set(taskId, configs);
|
|
781
|
+
}
|
|
782
|
+
configs.set(config.id, { ...config });
|
|
783
|
+
}
|
|
784
|
+
async get(taskId, configId) {
|
|
785
|
+
const cfg = this.byTask.get(taskId)?.get(configId);
|
|
786
|
+
return cfg ? { ...cfg } : void 0;
|
|
787
|
+
}
|
|
788
|
+
async list(taskId) {
|
|
789
|
+
const configs = this.byTask.get(taskId);
|
|
790
|
+
if (!configs) return [];
|
|
791
|
+
return [...configs.values()].map((c) => ({ ...c }));
|
|
792
|
+
}
|
|
793
|
+
async delete(taskId, configId) {
|
|
794
|
+
const configs = this.byTask.get(taskId);
|
|
795
|
+
if (!configs) return;
|
|
796
|
+
configs.delete(configId);
|
|
797
|
+
if (configs.size === 0) this.byTask.delete(taskId);
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
var PUSH_TABLE_DDL = (table) => `
|
|
801
|
+
CREATE TABLE IF NOT EXISTS ${table} (
|
|
802
|
+
task_id TEXT NOT NULL,
|
|
803
|
+
config_id TEXT NOT NULL,
|
|
804
|
+
url TEXT NOT NULL,
|
|
805
|
+
token TEXT,
|
|
806
|
+
authentication TEXT,
|
|
807
|
+
PRIMARY KEY (task_id, config_id)
|
|
808
|
+
)
|
|
809
|
+
`;
|
|
810
|
+
var SqlPushNotificationStore = class {
|
|
811
|
+
constructor(db, table = "a2a_push_configs") {
|
|
812
|
+
this.db = db;
|
|
813
|
+
this.table = table;
|
|
814
|
+
}
|
|
815
|
+
db;
|
|
816
|
+
table;
|
|
817
|
+
async migrate() {
|
|
818
|
+
await this.db.exec(PUSH_TABLE_DDL(this.table));
|
|
819
|
+
}
|
|
820
|
+
async set(taskId, config) {
|
|
821
|
+
const auth = config.authentication ? JSON.stringify(config.authentication) : null;
|
|
822
|
+
const updated = await this.db.exec(
|
|
823
|
+
`UPDATE ${this.table} SET url = ?, token = ?, authentication = ? WHERE task_id = ? AND config_id = ?`,
|
|
824
|
+
[config.url, config.token ?? null, auth, taskId, config.id]
|
|
825
|
+
);
|
|
826
|
+
if (updated.rowsAffected === 0) {
|
|
827
|
+
await this.db.exec(
|
|
828
|
+
`INSERT INTO ${this.table} (task_id, config_id, url, token, authentication) VALUES (?, ?, ?, ?, ?)`,
|
|
829
|
+
[taskId, config.id, config.url, config.token ?? null, auth]
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async get(taskId, configId) {
|
|
834
|
+
const rows = await this.db.query(
|
|
835
|
+
`SELECT config_id, url, token, authentication FROM ${this.table} WHERE task_id = ? AND config_id = ?`,
|
|
836
|
+
[taskId, configId]
|
|
837
|
+
);
|
|
838
|
+
const row = rows[0];
|
|
839
|
+
if (!row) return void 0;
|
|
840
|
+
return {
|
|
841
|
+
id: row.config_id,
|
|
842
|
+
url: row.url,
|
|
843
|
+
token: row.token ?? void 0,
|
|
844
|
+
authentication: row.authentication ? JSON.parse(row.authentication) : void 0
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async list(taskId) {
|
|
848
|
+
const rows = await this.db.query(
|
|
849
|
+
`SELECT config_id, url, token, authentication FROM ${this.table} WHERE task_id = ?`,
|
|
850
|
+
[taskId]
|
|
851
|
+
);
|
|
852
|
+
return rows.map((row) => ({
|
|
853
|
+
id: row.config_id,
|
|
854
|
+
url: row.url,
|
|
855
|
+
token: row.token ?? void 0,
|
|
856
|
+
authentication: row.authentication ? JSON.parse(row.authentication) : void 0
|
|
857
|
+
}));
|
|
858
|
+
}
|
|
859
|
+
async delete(taskId, configId) {
|
|
860
|
+
await this.db.exec(
|
|
861
|
+
`DELETE FROM ${this.table} WHERE task_id = ? AND config_id = ?`,
|
|
862
|
+
[taskId, configId]
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
async function deliverPushNotifications(args) {
|
|
867
|
+
const fetcher = args.fetcher ?? fetch;
|
|
868
|
+
const configs = await args.store.list(args.task.id);
|
|
869
|
+
const body = JSON.stringify({
|
|
870
|
+
taskId: args.task.id,
|
|
871
|
+
state: args.task.status.state,
|
|
872
|
+
task: args.task
|
|
873
|
+
});
|
|
874
|
+
const signature = args.webhookSecret ? `sha256=${await hmacSha256Hex(args.webhookSecret, body)}` : void 0;
|
|
875
|
+
const results = [];
|
|
876
|
+
for (const config of configs) {
|
|
877
|
+
const headers = { "Content-Type": "application/json" };
|
|
878
|
+
if (config.token) headers["X-A2A-Notification-Token"] = config.token;
|
|
879
|
+
if (signature) headers["X-A2A-Signature"] = signature;
|
|
880
|
+
if (config.authentication?.credentials) {
|
|
881
|
+
const scheme = config.authentication.schemes[0] ?? "Bearer";
|
|
882
|
+
headers.Authorization = `${scheme} ${config.authentication.credentials}`;
|
|
883
|
+
}
|
|
884
|
+
let result;
|
|
885
|
+
try {
|
|
886
|
+
const res = await fetcher(config.url, { method: "POST", headers, body });
|
|
887
|
+
result = {
|
|
888
|
+
taskId: args.task.id,
|
|
889
|
+
configId: config.id,
|
|
890
|
+
url: config.url,
|
|
891
|
+
ok: res.ok,
|
|
892
|
+
status: res.status
|
|
893
|
+
};
|
|
894
|
+
} catch (err) {
|
|
895
|
+
result = {
|
|
896
|
+
taskId: args.task.id,
|
|
897
|
+
configId: config.id,
|
|
898
|
+
url: config.url,
|
|
899
|
+
ok: false,
|
|
900
|
+
error: err instanceof Error ? err.message : String(err)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
args.onDelivery?.(result);
|
|
904
|
+
results.push(result);
|
|
905
|
+
}
|
|
906
|
+
return results;
|
|
907
|
+
}
|
|
908
|
+
async function hmacSha256Hex(secret, body) {
|
|
909
|
+
const enc = new TextEncoder();
|
|
910
|
+
const key = await crypto.subtle.importKey(
|
|
911
|
+
"raw",
|
|
912
|
+
enc.encode(secret),
|
|
913
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
914
|
+
false,
|
|
915
|
+
["sign"]
|
|
916
|
+
);
|
|
917
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
918
|
+
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// src/a2a/translate.ts
|
|
922
|
+
function extractTextFromMessage(message) {
|
|
923
|
+
if (!message || message.kind !== "message") {
|
|
924
|
+
return {
|
|
925
|
+
error: {
|
|
926
|
+
code: A2A_ERROR_CODES.INVALID_PARAMS,
|
|
927
|
+
message: 'params.message must be an A2A Message ({ kind: "message", role, parts, messageId })'
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
if (!Array.isArray(message.parts) || message.parts.length === 0) {
|
|
932
|
+
return {
|
|
933
|
+
error: {
|
|
934
|
+
code: A2A_ERROR_CODES.INVALID_PARAMS,
|
|
935
|
+
message: "message.parts must be a non-empty array"
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const texts = [];
|
|
940
|
+
for (const part of message.parts) {
|
|
941
|
+
if (part.kind === "text") {
|
|
942
|
+
if (typeof part.text !== "string") {
|
|
943
|
+
return {
|
|
944
|
+
error: {
|
|
945
|
+
code: A2A_ERROR_CODES.INVALID_PARAMS,
|
|
946
|
+
message: "text part .text must be a string"
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
texts.push(part.text);
|
|
951
|
+
} else {
|
|
952
|
+
return {
|
|
953
|
+
error: {
|
|
954
|
+
code: A2A_ERROR_CODES.CONTENT_TYPE_NOT_SUPPORTED,
|
|
955
|
+
message: `part.kind '${part.kind}' not supported; this agent accepts text parts only`
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return { text: texts.join("\n\n") };
|
|
961
|
+
}
|
|
962
|
+
function responseTextToArtifact(text, artifactId) {
|
|
963
|
+
return {
|
|
964
|
+
artifactId,
|
|
965
|
+
name: "response",
|
|
966
|
+
parts: [{ kind: "text", text }]
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/a2a/handler.ts
|
|
971
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
972
|
+
"completed",
|
|
973
|
+
"canceled",
|
|
974
|
+
"failed",
|
|
975
|
+
"rejected"
|
|
976
|
+
]);
|
|
977
|
+
var CancelRegistry = class {
|
|
978
|
+
controllers = /* @__PURE__ */ new Map();
|
|
979
|
+
register(taskId) {
|
|
980
|
+
const c = new AbortController();
|
|
981
|
+
this.controllers.set(taskId, c);
|
|
982
|
+
return c;
|
|
983
|
+
}
|
|
984
|
+
clear(taskId) {
|
|
985
|
+
this.controllers.delete(taskId);
|
|
986
|
+
}
|
|
987
|
+
cancel(taskId) {
|
|
988
|
+
const c = this.controllers.get(taskId);
|
|
989
|
+
if (!c) return false;
|
|
990
|
+
c.abort();
|
|
991
|
+
this.controllers.delete(taskId);
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
function createA2AHandlers(deps) {
|
|
996
|
+
const cancels = new CancelRegistry();
|
|
997
|
+
const handleAgentCard = async (c) => {
|
|
998
|
+
const slug = c.req.param("slug");
|
|
999
|
+
if (!slug) return c.json({ error: "slug required" }, 400);
|
|
1000
|
+
const agent = await deps.config.resolveAgent(slug);
|
|
1001
|
+
if (!agent || !agent.enabled) {
|
|
1002
|
+
return c.json({ error: "Agent not found or not published" }, 404);
|
|
1003
|
+
}
|
|
1004
|
+
const url = new URL(c.req.url);
|
|
1005
|
+
const agentUrl = `${url.origin}${url.pathname.replace(/\/\.well-known\/agent\.json$/, "")}`;
|
|
1006
|
+
return c.json(buildAgentCard(agent, deps.config, agentUrl));
|
|
1007
|
+
};
|
|
1008
|
+
const handleJsonRpc = async (c) => {
|
|
1009
|
+
const slug = c.req.param("slug");
|
|
1010
|
+
if (!slug) {
|
|
1011
|
+
return c.json(fail(null, A2A_ERROR_CODES.INVALID_REQUEST, "slug required"), 400);
|
|
1012
|
+
}
|
|
1013
|
+
const contentLength = Number.parseInt(c.req.header("Content-Length") ?? "0", 10);
|
|
1014
|
+
if (contentLength > 65536) {
|
|
1015
|
+
return c.json(fail(null, A2A_ERROR_CODES.INVALID_REQUEST, "request body too large (max 64KB)"), 413);
|
|
1016
|
+
}
|
|
1017
|
+
let raw;
|
|
1018
|
+
try {
|
|
1019
|
+
raw = await c.req.json();
|
|
1020
|
+
} catch {
|
|
1021
|
+
return c.json(fail(null, A2A_ERROR_CODES.PARSE_ERROR, "invalid JSON"), 400);
|
|
1022
|
+
}
|
|
1023
|
+
const parsed = parseEnvelope(raw);
|
|
1024
|
+
if ("code" in parsed) {
|
|
1025
|
+
return c.json(fail(parsed.id, parsed.code, parsed.message), 400);
|
|
1026
|
+
}
|
|
1027
|
+
switch (parsed.method) {
|
|
1028
|
+
case "message/send":
|
|
1029
|
+
return handleMessageSend(c, slug, parsed, deps);
|
|
1030
|
+
case "message/stream":
|
|
1031
|
+
return handleMessageStream(c, slug, parsed, deps, cancels);
|
|
1032
|
+
case "tasks/get":
|
|
1033
|
+
return handleTasksGet(c, parsed, deps);
|
|
1034
|
+
case "tasks/cancel":
|
|
1035
|
+
return handleTasksCancel(c, parsed, deps, cancels);
|
|
1036
|
+
case "tasks/resubscribe":
|
|
1037
|
+
return handleTasksResubscribe(c, parsed, deps);
|
|
1038
|
+
case "tasks/pushNotificationConfig/set":
|
|
1039
|
+
return handlePushSet(c, parsed, deps);
|
|
1040
|
+
case "tasks/pushNotificationConfig/get":
|
|
1041
|
+
return handlePushGet(c, parsed, deps);
|
|
1042
|
+
case "tasks/pushNotificationConfig/list":
|
|
1043
|
+
return handlePushList(c, parsed, deps);
|
|
1044
|
+
case "tasks/pushNotificationConfig/delete":
|
|
1045
|
+
return handlePushDelete(c, parsed, deps);
|
|
1046
|
+
default:
|
|
1047
|
+
return c.json(
|
|
1048
|
+
fail(parsed.id, A2A_ERROR_CODES.METHOD_NOT_FOUND, `unknown method '${parsed.method}'`)
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
return { handleAgentCard, handleJsonRpc };
|
|
1053
|
+
}
|
|
1054
|
+
async function handleMessageSend(c, slug, req, deps) {
|
|
1055
|
+
const guard = await guardMessageRequest(c, slug, req, deps);
|
|
1056
|
+
if (guard instanceof Response) return guard;
|
|
1057
|
+
const { authz, task } = guard;
|
|
1058
|
+
let responseText = "";
|
|
1059
|
+
let outputTokens = 0;
|
|
1060
|
+
let inputRequiredPrompt;
|
|
1061
|
+
let inputRequiredSeen = false;
|
|
1062
|
+
try {
|
|
1063
|
+
for await (const event of dispatchSandboxStreamRich(
|
|
1064
|
+
authz.agent,
|
|
1065
|
+
authz.userMessage,
|
|
1066
|
+
authz.consumerId,
|
|
1067
|
+
deps.config,
|
|
1068
|
+
void 0,
|
|
1069
|
+
task.id
|
|
1070
|
+
)) {
|
|
1071
|
+
if (event.kind === "text") {
|
|
1072
|
+
responseText += event.delta;
|
|
1073
|
+
outputTokens += estimateTokens(event.delta);
|
|
1074
|
+
} else {
|
|
1075
|
+
inputRequiredSeen = true;
|
|
1076
|
+
inputRequiredPrompt = event.prompt;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
} catch (err) {
|
|
1080
|
+
const failed = withStatus(task, "failed");
|
|
1081
|
+
await deps.taskStore.put(failed);
|
|
1082
|
+
await maybeDeliverPush(failed, deps);
|
|
1083
|
+
return c.json(
|
|
1084
|
+
fail(
|
|
1085
|
+
req.id,
|
|
1086
|
+
A2A_ERROR_CODES.INTERNAL_ERROR,
|
|
1087
|
+
err instanceof Error ? err.message : String(err)
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
await settleAndRecord(
|
|
1092
|
+
authz.agent,
|
|
1093
|
+
authz,
|
|
1094
|
+
estimateTokens(authz.userMessage),
|
|
1095
|
+
outputTokens,
|
|
1096
|
+
deps.config,
|
|
1097
|
+
deps.state.obs
|
|
1098
|
+
);
|
|
1099
|
+
if (inputRequiredSeen) {
|
|
1100
|
+
const paused = withStatus(
|
|
1101
|
+
task,
|
|
1102
|
+
"input-required",
|
|
1103
|
+
inputRequiredPrompt ? agentMessage(task, inputRequiredPrompt) : void 0,
|
|
1104
|
+
responseText ? [responseTextToArtifact(responseText, `${task.id}-artifact-0`)] : task.artifacts
|
|
1105
|
+
);
|
|
1106
|
+
await deps.taskStore.put(paused);
|
|
1107
|
+
return c.json(ok(req.id, paused));
|
|
1108
|
+
}
|
|
1109
|
+
const completed = withStatus(task, "completed", void 0, [
|
|
1110
|
+
responseTextToArtifact(responseText, `${task.id}-artifact-0`)
|
|
1111
|
+
]);
|
|
1112
|
+
await deps.taskStore.put(completed);
|
|
1113
|
+
await maybeDeliverPush(completed, deps);
|
|
1114
|
+
return c.json(ok(req.id, completed));
|
|
1115
|
+
}
|
|
1116
|
+
async function handleMessageStream(c, slug, req, deps, cancels) {
|
|
1117
|
+
const guard = await guardMessageRequest(c, slug, req, deps);
|
|
1118
|
+
if (guard instanceof Response) return guard;
|
|
1119
|
+
const { authz, task } = guard;
|
|
1120
|
+
const controller = cancels.register(task.id);
|
|
1121
|
+
const inputTokens = estimateTokens(authz.userMessage);
|
|
1122
|
+
let outputTokens = 0;
|
|
1123
|
+
let responseText = "";
|
|
1124
|
+
const stream = new ReadableStream({
|
|
1125
|
+
async start(ctrl) {
|
|
1126
|
+
const encoder = new TextEncoder();
|
|
1127
|
+
const send = (event) => {
|
|
1128
|
+
ctrl.enqueue(encoder.encode(`data: ${JSON.stringify(ok(req.id, event))}
|
|
1129
|
+
|
|
1130
|
+
`));
|
|
1131
|
+
};
|
|
1132
|
+
const workingStatus = {
|
|
1133
|
+
kind: "status-update",
|
|
1134
|
+
taskId: task.id,
|
|
1135
|
+
contextId: task.contextId,
|
|
1136
|
+
status: { state: "working", timestamp: nowIso() },
|
|
1137
|
+
final: false
|
|
1138
|
+
};
|
|
1139
|
+
await deps.taskStore.put({ ...task, status: workingStatus.status });
|
|
1140
|
+
send(workingStatus);
|
|
1141
|
+
let inputRequiredPrompt;
|
|
1142
|
+
let inputRequiredSeen = false;
|
|
1143
|
+
try {
|
|
1144
|
+
for await (const event of dispatchSandboxStreamRich(
|
|
1145
|
+
authz.agent,
|
|
1146
|
+
authz.userMessage,
|
|
1147
|
+
authz.consumerId,
|
|
1148
|
+
deps.config,
|
|
1149
|
+
controller.signal,
|
|
1150
|
+
task.id
|
|
1151
|
+
)) {
|
|
1152
|
+
if (event.kind === "text") {
|
|
1153
|
+
responseText += event.delta;
|
|
1154
|
+
outputTokens += estimateTokens(event.delta);
|
|
1155
|
+
const artifactEvent = {
|
|
1156
|
+
kind: "artifact-update",
|
|
1157
|
+
taskId: task.id,
|
|
1158
|
+
contextId: task.contextId,
|
|
1159
|
+
artifact: {
|
|
1160
|
+
artifactId: `${task.id}-artifact-0`,
|
|
1161
|
+
name: "response",
|
|
1162
|
+
parts: [{ kind: "text", text: event.delta }]
|
|
1163
|
+
},
|
|
1164
|
+
append: true
|
|
1165
|
+
};
|
|
1166
|
+
send(artifactEvent);
|
|
1167
|
+
} else {
|
|
1168
|
+
inputRequiredSeen = true;
|
|
1169
|
+
inputRequiredPrompt = event.prompt;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (controller.signal.aborted) {
|
|
1173
|
+
const canceled = withStatus(task, "canceled", void 0, [
|
|
1174
|
+
responseTextToArtifact(responseText, `${task.id}-artifact-0`)
|
|
1175
|
+
]);
|
|
1176
|
+
await deps.taskStore.put(canceled);
|
|
1177
|
+
send({
|
|
1178
|
+
kind: "status-update",
|
|
1179
|
+
taskId: task.id,
|
|
1180
|
+
contextId: task.contextId,
|
|
1181
|
+
status: canceled.status,
|
|
1182
|
+
final: true
|
|
1183
|
+
});
|
|
1184
|
+
await maybeDeliverPush(canceled, deps);
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
await settleAndRecord(
|
|
1188
|
+
authz.agent,
|
|
1189
|
+
authz,
|
|
1190
|
+
inputTokens,
|
|
1191
|
+
outputTokens,
|
|
1192
|
+
deps.config,
|
|
1193
|
+
deps.state.obs
|
|
1194
|
+
);
|
|
1195
|
+
if (inputRequiredSeen) {
|
|
1196
|
+
const paused = withStatus(
|
|
1197
|
+
task,
|
|
1198
|
+
"input-required",
|
|
1199
|
+
inputRequiredPrompt ? agentMessage(task, inputRequiredPrompt) : void 0,
|
|
1200
|
+
responseText ? [responseTextToArtifact(responseText, `${task.id}-artifact-0`)] : task.artifacts
|
|
1201
|
+
);
|
|
1202
|
+
await deps.taskStore.put(paused);
|
|
1203
|
+
send({
|
|
1204
|
+
kind: "status-update",
|
|
1205
|
+
taskId: task.id,
|
|
1206
|
+
contextId: task.contextId,
|
|
1207
|
+
status: paused.status,
|
|
1208
|
+
final: true
|
|
1209
|
+
});
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
send({
|
|
1213
|
+
kind: "artifact-update",
|
|
1214
|
+
taskId: task.id,
|
|
1215
|
+
contextId: task.contextId,
|
|
1216
|
+
artifact: {
|
|
1217
|
+
artifactId: `${task.id}-artifact-0`,
|
|
1218
|
+
name: "response",
|
|
1219
|
+
parts: [{ kind: "text", text: "" }]
|
|
1220
|
+
},
|
|
1221
|
+
append: true,
|
|
1222
|
+
lastChunk: true
|
|
1223
|
+
});
|
|
1224
|
+
const completed = withStatus(task, "completed", void 0, [
|
|
1225
|
+
responseTextToArtifact(responseText, `${task.id}-artifact-0`)
|
|
1226
|
+
]);
|
|
1227
|
+
await deps.taskStore.put(completed);
|
|
1228
|
+
send({
|
|
1229
|
+
kind: "status-update",
|
|
1230
|
+
taskId: task.id,
|
|
1231
|
+
contextId: task.contextId,
|
|
1232
|
+
status: completed.status,
|
|
1233
|
+
final: true
|
|
1234
|
+
});
|
|
1235
|
+
await maybeDeliverPush(completed, deps);
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
const failed = withStatus(task, "failed");
|
|
1238
|
+
await deps.taskStore.put(failed);
|
|
1239
|
+
send({
|
|
1240
|
+
kind: "status-update",
|
|
1241
|
+
taskId: task.id,
|
|
1242
|
+
contextId: task.contextId,
|
|
1243
|
+
status: failed.status,
|
|
1244
|
+
final: true
|
|
1245
|
+
});
|
|
1246
|
+
await maybeDeliverPush(failed, deps);
|
|
1247
|
+
await deps.state.obs?.onStreamError?.(
|
|
1248
|
+
{
|
|
1249
|
+
requestId: authz.requestId,
|
|
1250
|
+
agentSlug: authz.agent.slug,
|
|
1251
|
+
startMs: authz.startMs
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
consumerId: authz.consumerId,
|
|
1255
|
+
errorMessage: err instanceof Error ? err.message : String(err)
|
|
1256
|
+
}
|
|
1257
|
+
);
|
|
1258
|
+
} finally {
|
|
1259
|
+
cancels.clear(task.id);
|
|
1260
|
+
ctrl.close();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
return new Response(stream, {
|
|
1265
|
+
headers: {
|
|
1266
|
+
"Content-Type": "text/event-stream",
|
|
1267
|
+
"Cache-Control": "no-cache",
|
|
1268
|
+
"X-Request-Id": authz.requestId,
|
|
1269
|
+
"X-Agent-Slug": authz.agent.slug,
|
|
1270
|
+
"X-Task-Id": task.id
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
async function handleTasksGet(c, req, deps) {
|
|
1275
|
+
const params = req.params;
|
|
1276
|
+
if (!params || typeof params.id !== "string") {
|
|
1277
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
|
|
1278
|
+
}
|
|
1279
|
+
const task = await deps.taskStore.get(params.id);
|
|
1280
|
+
if (!task) {
|
|
1281
|
+
return c.json(
|
|
1282
|
+
fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`)
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
return c.json(ok(req.id, task));
|
|
1286
|
+
}
|
|
1287
|
+
async function handleTasksCancel(c, req, deps, cancels) {
|
|
1288
|
+
const params = req.params;
|
|
1289
|
+
if (!params || typeof params.id !== "string") {
|
|
1290
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
|
|
1291
|
+
}
|
|
1292
|
+
const task = await deps.taskStore.get(params.id);
|
|
1293
|
+
if (!task) {
|
|
1294
|
+
return c.json(
|
|
1295
|
+
fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`)
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (isTerminal(task.status.state)) {
|
|
1299
|
+
return c.json(
|
|
1300
|
+
fail(
|
|
1301
|
+
req.id,
|
|
1302
|
+
A2A_ERROR_CODES.TASK_NOT_CANCELABLE,
|
|
1303
|
+
`task '${params.id}' is in terminal state '${task.status.state}'`
|
|
1304
|
+
)
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
const stillActive = cancels.cancel(task.id);
|
|
1308
|
+
const canceled = {
|
|
1309
|
+
...task,
|
|
1310
|
+
status: { state: "canceled", timestamp: nowIso() }
|
|
1311
|
+
};
|
|
1312
|
+
await deps.taskStore.put(canceled);
|
|
1313
|
+
if (!stillActive) {
|
|
1314
|
+
await maybeDeliverPush(canceled, deps);
|
|
1315
|
+
}
|
|
1316
|
+
return c.json(ok(req.id, canceled));
|
|
1317
|
+
}
|
|
1318
|
+
async function handleTasksResubscribe(c, req, deps) {
|
|
1319
|
+
const params = req.params;
|
|
1320
|
+
if (!params || typeof params.id !== "string") {
|
|
1321
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
|
|
1322
|
+
}
|
|
1323
|
+
const task = await deps.taskStore.get(params.id);
|
|
1324
|
+
if (!task) {
|
|
1325
|
+
return c.json(
|
|
1326
|
+
fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.id}' not found`)
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
const final = isTerminal(task.status.state) || task.status.state === "input-required";
|
|
1330
|
+
const event = {
|
|
1331
|
+
kind: "status-update",
|
|
1332
|
+
taskId: task.id,
|
|
1333
|
+
contextId: task.contextId,
|
|
1334
|
+
status: task.status,
|
|
1335
|
+
final
|
|
1336
|
+
};
|
|
1337
|
+
const encoder = new TextEncoder();
|
|
1338
|
+
const stream = new ReadableStream({
|
|
1339
|
+
start(ctrl) {
|
|
1340
|
+
ctrl.enqueue(encoder.encode(`data: ${JSON.stringify(ok(req.id, event))}
|
|
1341
|
+
|
|
1342
|
+
`));
|
|
1343
|
+
ctrl.close();
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
return new Response(stream, {
|
|
1347
|
+
headers: {
|
|
1348
|
+
"Content-Type": "text/event-stream",
|
|
1349
|
+
"Cache-Control": "no-cache",
|
|
1350
|
+
"X-Task-Id": task.id
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
async function handlePushSet(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.taskId !== "string" || !params.pushNotificationConfig?.id) {
|
|
1360
|
+
return c.json(
|
|
1361
|
+
fail(
|
|
1362
|
+
req.id,
|
|
1363
|
+
A2A_ERROR_CODES.INVALID_PARAMS,
|
|
1364
|
+
"params.taskId and params.pushNotificationConfig.id required"
|
|
1365
|
+
)
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof params.pushNotificationConfig.url !== "string") {
|
|
1369
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "pushNotificationConfig.url required"));
|
|
1370
|
+
}
|
|
1371
|
+
const task = await deps.taskStore.get(params.taskId);
|
|
1372
|
+
if (!task) {
|
|
1373
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.TASK_NOT_FOUND, `task '${params.taskId}' not found`));
|
|
1374
|
+
}
|
|
1375
|
+
await deps.pushStore.set(params.taskId, params.pushNotificationConfig);
|
|
1376
|
+
const stored = await deps.pushStore.get(params.taskId, params.pushNotificationConfig.id);
|
|
1377
|
+
return c.json(ok(req.id, { taskId: params.taskId, pushNotificationConfig: stored }));
|
|
1378
|
+
}
|
|
1379
|
+
async function handlePushGet(c, req, deps) {
|
|
1380
|
+
if (!deps.pushStore) {
|
|
1381
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
|
|
1382
|
+
}
|
|
1383
|
+
const params = req.params;
|
|
1384
|
+
if (!params || typeof params.id !== "string" || typeof params.pushNotificationConfigId !== "string") {
|
|
1385
|
+
return c.json(
|
|
1386
|
+
fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id and params.pushNotificationConfigId required")
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const cfg = await deps.pushStore.get(params.id, params.pushNotificationConfigId);
|
|
1390
|
+
if (!cfg) {
|
|
1391
|
+
return c.json(
|
|
1392
|
+
fail(
|
|
1393
|
+
req.id,
|
|
1394
|
+
A2A_ERROR_CODES.TASK_NOT_FOUND,
|
|
1395
|
+
`push config '${params.pushNotificationConfigId}' not found for task '${params.id}'`
|
|
1396
|
+
)
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
return c.json(ok(req.id, { taskId: params.id, pushNotificationConfig: cfg }));
|
|
1400
|
+
}
|
|
1401
|
+
async function handlePushList(c, req, deps) {
|
|
1402
|
+
if (!deps.pushStore) {
|
|
1403
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
|
|
1404
|
+
}
|
|
1405
|
+
const params = req.params;
|
|
1406
|
+
if (!params || typeof params.id !== "string") {
|
|
1407
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id required"));
|
|
1408
|
+
}
|
|
1409
|
+
const configs = await deps.pushStore.list(params.id);
|
|
1410
|
+
return c.json(ok(req.id, configs.map((cfg) => ({ taskId: params.id, pushNotificationConfig: cfg }))));
|
|
1411
|
+
}
|
|
1412
|
+
async function handlePushDelete(c, req, deps) {
|
|
1413
|
+
if (!deps.pushStore) {
|
|
1414
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.PUSH_NOT_SUPPORTED, "push notifications not configured"));
|
|
1415
|
+
}
|
|
1416
|
+
const params = req.params;
|
|
1417
|
+
if (!params || typeof params.id !== "string" || typeof params.pushNotificationConfigId !== "string") {
|
|
1418
|
+
return c.json(
|
|
1419
|
+
fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.id and params.pushNotificationConfigId required")
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
await deps.pushStore.delete(params.id, params.pushNotificationConfigId);
|
|
1423
|
+
return c.json(ok(req.id, null));
|
|
1424
|
+
}
|
|
1425
|
+
async function guardMessageRequest(c, slug, req, deps) {
|
|
1426
|
+
const params = req.params;
|
|
1427
|
+
if (!params || !params.message) {
|
|
1428
|
+
return c.json(fail(req.id, A2A_ERROR_CODES.INVALID_PARAMS, "params.message required"));
|
|
1429
|
+
}
|
|
1430
|
+
const extracted = extractTextFromMessage(params.message);
|
|
1431
|
+
if ("error" in extracted) {
|
|
1432
|
+
return c.json(fail(req.id, extracted.error.code, extracted.error.message));
|
|
1433
|
+
}
|
|
1434
|
+
const guard = await authenticateAndGuard(
|
|
1435
|
+
c,
|
|
1436
|
+
slug,
|
|
1437
|
+
[{ role: "user", content: extracted.text }],
|
|
1438
|
+
deps.config,
|
|
1439
|
+
deps.state
|
|
1440
|
+
);
|
|
1441
|
+
if (guard instanceof Response) return guard;
|
|
1442
|
+
const authz = guard;
|
|
1443
|
+
if (typeof params.message.taskId === "string") {
|
|
1444
|
+
const existing = await deps.taskStore.get(params.message.taskId);
|
|
1445
|
+
if (existing) {
|
|
1446
|
+
if (existing.status.state !== "input-required") {
|
|
1447
|
+
return c.json(
|
|
1448
|
+
fail(
|
|
1449
|
+
req.id,
|
|
1450
|
+
A2A_ERROR_CODES.INVALID_PARAMS,
|
|
1451
|
+
`task '${existing.id}' is in state '${existing.status.state}'; only 'input-required' tasks accept follow-up messages`
|
|
1452
|
+
)
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
const appendedMessage = {
|
|
1456
|
+
...params.message,
|
|
1457
|
+
taskId: existing.id,
|
|
1458
|
+
contextId: existing.contextId
|
|
1459
|
+
};
|
|
1460
|
+
const continued = {
|
|
1461
|
+
...existing,
|
|
1462
|
+
status: { state: "working", timestamp: nowIso() },
|
|
1463
|
+
history: [...existing.history ?? [], appendedMessage]
|
|
1464
|
+
};
|
|
1465
|
+
await deps.taskStore.put(continued);
|
|
1466
|
+
return { authz, task: continued };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
const taskId = params.message.taskId ?? `task_${cryptoRandomId()}`;
|
|
1470
|
+
const contextId = params.message.contextId ?? `ctx_${cryptoRandomId()}`;
|
|
1471
|
+
const initialMessage = {
|
|
1472
|
+
...params.message,
|
|
1473
|
+
taskId,
|
|
1474
|
+
contextId
|
|
1475
|
+
};
|
|
1476
|
+
const task = {
|
|
1477
|
+
kind: "task",
|
|
1478
|
+
id: taskId,
|
|
1479
|
+
contextId,
|
|
1480
|
+
status: { state: "submitted", timestamp: nowIso() },
|
|
1481
|
+
history: [initialMessage]
|
|
1482
|
+
};
|
|
1483
|
+
await deps.taskStore.put(task);
|
|
1484
|
+
return { authz, task };
|
|
1485
|
+
}
|
|
1486
|
+
function isTerminal(state) {
|
|
1487
|
+
return state === "completed" || state === "canceled" || state === "failed" || state === "rejected";
|
|
1488
|
+
}
|
|
1489
|
+
function nowIso() {
|
|
1490
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1491
|
+
}
|
|
1492
|
+
function cryptoRandomId() {
|
|
1493
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
1494
|
+
}
|
|
1495
|
+
function withStatus(task, state, message, artifacts) {
|
|
1496
|
+
return {
|
|
1497
|
+
...task,
|
|
1498
|
+
status: { state, timestamp: nowIso(), ...message ? { message } : {} },
|
|
1499
|
+
...artifacts !== void 0 ? { artifacts } : {}
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
function agentMessage(task, text) {
|
|
1503
|
+
return {
|
|
1504
|
+
kind: "message",
|
|
1505
|
+
role: "agent",
|
|
1506
|
+
parts: [{ kind: "text", text }],
|
|
1507
|
+
messageId: `${task.id}-status-${task.status.state}-${nowIso()}`,
|
|
1508
|
+
taskId: task.id,
|
|
1509
|
+
contextId: task.contextId
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
async function maybeDeliverPush(task, deps) {
|
|
1513
|
+
if (!deps.pushStore || !TERMINAL_STATES.has(task.status.state)) return;
|
|
1514
|
+
try {
|
|
1515
|
+
await deliverPushNotifications({
|
|
1516
|
+
task,
|
|
1517
|
+
store: deps.pushStore,
|
|
1518
|
+
webhookSecret: deps.config.a2a?.webhookSecret,
|
|
1519
|
+
fetcher: deps.config.a2a?.pushFetcher,
|
|
1520
|
+
onDelivery: (result) => {
|
|
1521
|
+
if (!result.ok) {
|
|
1522
|
+
deps.state.obs?.onStreamError?.(
|
|
1523
|
+
{ requestId: result.taskId, agentSlug: task.id, startMs: Date.now() },
|
|
1524
|
+
{
|
|
1525
|
+
consumerId: result.configId,
|
|
1526
|
+
errorMessage: `push delivery failed (${result.status ?? "no-status"}): ${result.error ?? "non-2xx"}`
|
|
1527
|
+
}
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
console.error(
|
|
1534
|
+
`[agent-gateway] push delivery threw for task ${task.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
// src/a2a/task-store.ts
|
|
1540
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
1541
|
+
var InMemoryTaskStore = class {
|
|
1542
|
+
constructor(ttlMs = DEFAULT_TTL_MS) {
|
|
1543
|
+
this.ttlMs = ttlMs;
|
|
1544
|
+
}
|
|
1545
|
+
ttlMs;
|
|
1546
|
+
entries = /* @__PURE__ */ new Map();
|
|
1547
|
+
async get(id) {
|
|
1548
|
+
this.gc();
|
|
1549
|
+
const entry = this.entries.get(id);
|
|
1550
|
+
if (!entry) return void 0;
|
|
1551
|
+
return clone(entry.task);
|
|
1552
|
+
}
|
|
1553
|
+
async put(task) {
|
|
1554
|
+
this.gc();
|
|
1555
|
+
this.entries.set(task.id, { task: clone(task), expiresAt: Date.now() + this.ttlMs });
|
|
1556
|
+
}
|
|
1557
|
+
async delete(id) {
|
|
1558
|
+
this.entries.delete(id);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Sweep expired tasks. Called inline on every read/write — cheap for the
|
|
1562
|
+
* Map sizes this is designed for (10s–1000s of concurrent tasks).
|
|
1563
|
+
*/
|
|
1564
|
+
gc() {
|
|
1565
|
+
const now = Date.now();
|
|
1566
|
+
for (const [id, entry] of this.entries) {
|
|
1567
|
+
if (entry.expiresAt <= now) this.entries.delete(id);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
function clone(value) {
|
|
1572
|
+
return JSON.parse(JSON.stringify(value));
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/middleware.ts
|
|
1576
|
+
function createAgentGateway(config) {
|
|
1577
|
+
if (!config.x402.verifySigner && !config.x402.demoMode) {
|
|
1578
|
+
throw new Error(
|
|
1579
|
+
"createAgentGateway: x402.verifySigner is required in production. For tests, set x402.demoMode: true explicitly."
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
const gw = new Hono();
|
|
1583
|
+
const rateLimitStore = config.rateLimitStore ?? new MemoryRateLimitStore();
|
|
1584
|
+
const state = {
|
|
1585
|
+
rateLimitStore,
|
|
1586
|
+
nonceStore: config.nonceStore ?? new MemoryNonceStore(),
|
|
1587
|
+
globalRateLimit: config.rateLimit ?? { limit: 60, windowSeconds: 60 },
|
|
1588
|
+
requiredScope: config.requiredScope ?? "chat",
|
|
1589
|
+
maxLen: config.maxMessageLength ?? 8e3,
|
|
1590
|
+
obs: config.observer
|
|
1591
|
+
};
|
|
1592
|
+
const obs = state.obs;
|
|
1593
|
+
gw.get("/:slug/chat/completions", async (c) => {
|
|
1594
|
+
const slug = c.req.param("slug");
|
|
1595
|
+
const agent = await config.resolveAgent(slug);
|
|
1596
|
+
if (!agent || !agent.enabled) return c.json({ error: "Agent not found or not published" }, 404);
|
|
1597
|
+
const paymentMethods = [
|
|
1598
|
+
{
|
|
1599
|
+
type: "x402",
|
|
1600
|
+
operator: config.x402.operatorAddress,
|
|
1601
|
+
chain_id: config.x402.chainId,
|
|
1602
|
+
credits_contract: config.x402.creditsAddress
|
|
1603
|
+
}
|
|
1604
|
+
];
|
|
1605
|
+
if (isMppAuthEnabled(config)) {
|
|
1606
|
+
paymentMethods.push({
|
|
1607
|
+
type: "mpp",
|
|
1608
|
+
realm: config.mpp.realm,
|
|
1609
|
+
method: config.mpp.method ?? "blueprintevm"
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
if (isApiKeyAuthEnabled(config)) paymentMethods.push({ type: "api_key", prefix: "sk_agent_" });
|
|
1613
|
+
return c.json({
|
|
1614
|
+
slug: agent.slug,
|
|
1615
|
+
pricing: {
|
|
1616
|
+
per_token_usd: agent.pricePerTokenUsd,
|
|
1617
|
+
currency: "USD",
|
|
1618
|
+
platform_fee_percent: agent.platformFeePercent
|
|
1619
|
+
},
|
|
1620
|
+
hosting: {
|
|
1621
|
+
mode: agent.sandboxEndpoint ? "sovereign" : "centralized",
|
|
1622
|
+
endpoint: agent.sandboxEndpoint ?? config.baseUrl ?? "tangle.tools"
|
|
1623
|
+
},
|
|
1624
|
+
payment_methods: paymentMethods,
|
|
1625
|
+
capabilities: ["chat.completions", "streaming"],
|
|
1626
|
+
openai_compatible: true
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
gw.post("/:slug/chat/completions", async (c) => {
|
|
1630
|
+
const slug = c.req.param("slug");
|
|
1631
|
+
const contentLength = Number.parseInt(c.req.header("Content-Length") ?? "0", 10);
|
|
1632
|
+
if (contentLength > 65536) {
|
|
1633
|
+
const requestId = generateRequestId();
|
|
1634
|
+
await obs?.onBodyTooLarge?.(
|
|
1635
|
+
{ requestId, agentSlug: slug, startMs: Date.now() },
|
|
1636
|
+
contentLength
|
|
1637
|
+
);
|
|
1638
|
+
return c.json(
|
|
1639
|
+
{
|
|
1640
|
+
error: {
|
|
1641
|
+
message: "Request body too large (max 64KB)",
|
|
1642
|
+
type: "invalid_request"
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
413
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
let body;
|
|
1649
|
+
try {
|
|
1650
|
+
body = await c.req.json();
|
|
1651
|
+
} catch {
|
|
1652
|
+
return c.json({ error: { message: "Invalid JSON", type: "invalid_request" } }, 400);
|
|
1653
|
+
}
|
|
1654
|
+
if (!body.messages?.length) {
|
|
1655
|
+
return c.json(
|
|
1656
|
+
{ error: { message: "messages array required", type: "invalid_request" } },
|
|
1657
|
+
400
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
const guard = await authenticateAndGuard(c, slug, body.messages, config, state);
|
|
1661
|
+
if (guard instanceof Response) return guard;
|
|
1662
|
+
const authz = guard;
|
|
1663
|
+
return streamChatCompletions(c, authz, config, obs);
|
|
1664
|
+
});
|
|
1665
|
+
const taskStore = config.a2a?.taskStore ?? new InMemoryTaskStore();
|
|
1666
|
+
const pushStore = config.a2a?.pushStore;
|
|
1667
|
+
const a2a = createA2AHandlers({ config, state, taskStore, pushStore });
|
|
1668
|
+
gw.get("/:slug/.well-known/agent.json", a2a.handleAgentCard);
|
|
1669
|
+
gw.post("/:slug", a2a.handleJsonRpc);
|
|
1670
|
+
return gw;
|
|
1671
|
+
}
|
|
1672
|
+
function streamChatCompletions(c, authz, config, obs) {
|
|
1673
|
+
const { agent, consumerId, paymentMethod, requestId, userMessage, rateLimitRemaining } = authz;
|
|
1674
|
+
const inputTokens = estimateTokens(userMessage);
|
|
1675
|
+
let outputTokens = 0;
|
|
1676
|
+
const ctx = {
|
|
1677
|
+
requestId,
|
|
1678
|
+
agentSlug: agent.slug,
|
|
1679
|
+
startMs: authz.startMs
|
|
1680
|
+
};
|
|
1681
|
+
const stream = new ReadableStream({
|
|
1682
|
+
async start(controller) {
|
|
1683
|
+
const encoder = new TextEncoder();
|
|
1684
|
+
const sendChunk = (delta) => {
|
|
1685
|
+
outputTokens += estimateTokens(delta);
|
|
1686
|
+
const chunk = {
|
|
1687
|
+
id: `chatcmpl-${Date.now()}`,
|
|
1688
|
+
object: "chat.completion.chunk",
|
|
1689
|
+
created: Math.floor(Date.now() / 1e3),
|
|
1690
|
+
model: agent.slug,
|
|
1691
|
+
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }]
|
|
1692
|
+
};
|
|
1693
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}
|
|
1694
|
+
|
|
1695
|
+
`));
|
|
1696
|
+
};
|
|
1697
|
+
try {
|
|
1698
|
+
for await (const delta of dispatchSandboxStream(agent, userMessage, consumerId, config)) {
|
|
1699
|
+
sendChunk(delta);
|
|
1700
|
+
}
|
|
1701
|
+
const done = {
|
|
1702
|
+
id: `chatcmpl-${Date.now()}`,
|
|
1703
|
+
object: "chat.completion.chunk",
|
|
1704
|
+
created: Math.floor(Date.now() / 1e3),
|
|
1705
|
+
model: agent.slug,
|
|
1706
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
1707
|
+
};
|
|
1708
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(done)}
|
|
1709
|
+
|
|
1710
|
+
`));
|
|
1711
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
1712
|
+
await settleAndRecord(agent, authz, inputTokens, outputTokens, config, obs);
|
|
1713
|
+
} catch (err) {
|
|
1714
|
+
const rawMessage = err instanceof Error ? err.message : String(err);
|
|
1715
|
+
const safeMessage = rawMessage.includes("/") || rawMessage.includes("\\") ? "Internal agent error" : rawMessage;
|
|
1716
|
+
await obs?.onStreamError?.(ctx, { consumerId, errorMessage: rawMessage });
|
|
1717
|
+
controller.enqueue(
|
|
1718
|
+
encoder.encode(
|
|
1719
|
+
`data: ${JSON.stringify({ error: { message: safeMessage, type: "server_error" } })}
|
|
1720
|
+
|
|
1721
|
+
`
|
|
1722
|
+
)
|
|
1723
|
+
);
|
|
1724
|
+
} finally {
|
|
1725
|
+
controller.close();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
return new Response(stream, {
|
|
1730
|
+
headers: {
|
|
1731
|
+
"Content-Type": "text/event-stream",
|
|
1732
|
+
"Cache-Control": "no-cache",
|
|
1733
|
+
"X-Request-Id": requestId,
|
|
1734
|
+
"X-Agent-Slug": agent.slug,
|
|
1735
|
+
"X-Agent-Hosting": agent.sandboxEndpoint ? "sovereign" : "centralized",
|
|
1736
|
+
"X-Payment-Method": paymentMethod,
|
|
1737
|
+
"X-Payment-Settled": paymentMethod === "x402" ? "pending" : "true",
|
|
1738
|
+
...rateLimitRemaining !== void 0 ? { "X-RateLimit-Remaining": String(rateLimitRemaining) } : {}
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
export {
|
|
1744
|
+
detectInjection,
|
|
1745
|
+
filterConsumerMessages,
|
|
1746
|
+
filterConsumerMessagesStrict,
|
|
1747
|
+
redactSystemPromptFromOutput,
|
|
1748
|
+
ConsoleObserver,
|
|
1749
|
+
CompositeObserver,
|
|
1750
|
+
generateRequestId,
|
|
1751
|
+
isApiKeyAuthEnabled,
|
|
1752
|
+
isMppAuthEnabled,
|
|
1753
|
+
verifyX402,
|
|
1754
|
+
verifyMpp,
|
|
1755
|
+
defaultVerifyApiKey,
|
|
1756
|
+
A2A_ERROR_CODES,
|
|
1757
|
+
InMemoryPushNotificationStore,
|
|
1758
|
+
SqlPushNotificationStore,
|
|
1759
|
+
deliverPushNotifications,
|
|
1760
|
+
InMemoryTaskStore,
|
|
1761
|
+
createAgentGateway
|
|
1762
|
+
};
|
|
1763
|
+
//# sourceMappingURL=chunk-Q4YAIEZY.js.map
|