@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,862 @@
1
+ import { NonceStore } from './nonce-store.js';
2
+ import { RateLimitStore } from './rate-limit.js';
3
+
4
+ /**
5
+ * A2A protocol types (Google Agent-to-Agent, April 2025).
6
+ *
7
+ * Subset shipped by this gateway:
8
+ * - Discovery: AgentCard via `.well-known/agent.json`
9
+ * - Messaging: `message/send`, `message/stream`
10
+ * - Task control: `tasks/get`, `tasks/cancel`, `tasks/resubscribe`
11
+ * - Push: `tasks/pushNotificationConfig/{set,get,list,delete}` (gated on `pushStore`)
12
+ * - Multi-turn: `input-required` state + follow-up `message/send` with the same `taskId`
13
+ * - Capabilities: streaming = true; pushNotifications gated on config; stateTransitionHistory = false
14
+ * - Parts: text only on input/output (data/file parts rejected with CONTENT_TYPE_NOT_SUPPORTED)
15
+ *
16
+ * Deferred until a real consumer needs them: authenticated extended card,
17
+ * data/file parts, OAuth2/mTLS auth schemes.
18
+ */
19
+ interface JSONRPCRequest {
20
+ jsonrpc: '2.0';
21
+ id: string | number | null;
22
+ method: string;
23
+ params?: unknown;
24
+ }
25
+ interface JSONRPCSuccessResponse<T = unknown> {
26
+ jsonrpc: '2.0';
27
+ id: string | number | null;
28
+ result: T;
29
+ }
30
+ interface JSONRPCErrorResponse {
31
+ jsonrpc: '2.0';
32
+ id: string | number | null;
33
+ error: {
34
+ code: number;
35
+ message: string;
36
+ data?: unknown;
37
+ };
38
+ }
39
+ type JSONRPCResponse<T = unknown> = JSONRPCSuccessResponse<T> | JSONRPCErrorResponse;
40
+ /** Standard JSON-RPC + A2A-specific codes. Negative ints per JSON-RPC spec. */
41
+ declare const A2A_ERROR_CODES: {
42
+ readonly PARSE_ERROR: -32700;
43
+ readonly INVALID_REQUEST: -32600;
44
+ readonly METHOD_NOT_FOUND: -32601;
45
+ readonly INVALID_PARAMS: -32602;
46
+ readonly INTERNAL_ERROR: -32603;
47
+ readonly TASK_NOT_FOUND: -32001;
48
+ readonly TASK_NOT_CANCELABLE: -32002;
49
+ readonly PUSH_NOT_SUPPORTED: -32003;
50
+ readonly UNSUPPORTED_OPERATION: -32004;
51
+ readonly CONTENT_TYPE_NOT_SUPPORTED: -32005;
52
+ readonly INVALID_AGENT_RESPONSE: -32006;
53
+ readonly AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED: -32007;
54
+ };
55
+ interface TextPart {
56
+ kind: 'text';
57
+ text: string;
58
+ metadata?: Record<string, unknown>;
59
+ }
60
+ interface DataPart {
61
+ kind: 'data';
62
+ data: Record<string, unknown>;
63
+ metadata?: Record<string, unknown>;
64
+ }
65
+ interface FilePart {
66
+ kind: 'file';
67
+ file: {
68
+ name?: string;
69
+ mimeType?: string;
70
+ bytes?: string;
71
+ uri?: string;
72
+ };
73
+ metadata?: Record<string, unknown>;
74
+ }
75
+ type Part = TextPart | DataPart | FilePart;
76
+ interface Message {
77
+ kind: 'message';
78
+ role: 'user' | 'agent';
79
+ parts: Part[];
80
+ messageId: string;
81
+ taskId?: string;
82
+ contextId?: string;
83
+ metadata?: Record<string, unknown>;
84
+ }
85
+ type TaskState = 'submitted' | 'working' | 'input-required' | 'completed' | 'canceled' | 'failed' | 'rejected' | 'auth-required';
86
+ interface TaskStatus {
87
+ state: TaskState;
88
+ message?: Message;
89
+ timestamp: string;
90
+ }
91
+ interface Artifact {
92
+ artifactId: string;
93
+ name?: string;
94
+ description?: string;
95
+ parts: Part[];
96
+ metadata?: Record<string, unknown>;
97
+ }
98
+ interface Task {
99
+ kind: 'task';
100
+ id: string;
101
+ contextId: string;
102
+ status: TaskStatus;
103
+ history?: Message[];
104
+ artifacts?: Artifact[];
105
+ metadata?: Record<string, unknown>;
106
+ }
107
+ interface TaskStatusUpdateEvent {
108
+ kind: 'status-update';
109
+ taskId: string;
110
+ contextId: string;
111
+ status: TaskStatus;
112
+ /** True on the terminal event; clients close the stream after this. */
113
+ final: boolean;
114
+ metadata?: Record<string, unknown>;
115
+ }
116
+ interface TaskArtifactUpdateEvent {
117
+ kind: 'artifact-update';
118
+ taskId: string;
119
+ contextId: string;
120
+ artifact: Artifact;
121
+ /** True when this artifact's parts should be appended to the prior emit (incremental streaming). */
122
+ append?: boolean;
123
+ /** True on the artifact's final chunk. */
124
+ lastChunk?: boolean;
125
+ metadata?: Record<string, unknown>;
126
+ }
127
+ type StreamingEvent = TaskStatusUpdateEvent | TaskArtifactUpdateEvent;
128
+ interface MessageSendParams {
129
+ message: Message;
130
+ configuration?: {
131
+ acceptedOutputModes?: string[];
132
+ blocking?: boolean;
133
+ historyLength?: number;
134
+ };
135
+ }
136
+ interface TaskIdParams {
137
+ id: string;
138
+ metadata?: Record<string, unknown>;
139
+ }
140
+ interface TaskPushNotificationConfigGetParams {
141
+ /** Task id whose configs are being queried. */
142
+ id: string;
143
+ /** Specific config id to fetch. Required for `set` and `delete`; omitted for `list`. */
144
+ pushNotificationConfigId?: string;
145
+ metadata?: Record<string, unknown>;
146
+ }
147
+ interface AgentSkill {
148
+ id: string;
149
+ name: string;
150
+ description: string;
151
+ tags?: string[];
152
+ examples?: string[];
153
+ inputModes?: string[];
154
+ outputModes?: string[];
155
+ }
156
+ interface AgentCapabilities {
157
+ streaming?: boolean;
158
+ pushNotifications?: boolean;
159
+ stateTransitionHistory?: boolean;
160
+ }
161
+ interface AgentProvider {
162
+ organization: string;
163
+ url?: string;
164
+ }
165
+ interface AgentCardAuthentication {
166
+ /** Auth scheme names the agent accepts (e.g. 'Bearer', 'x402', 'mpp'). */
167
+ schemes: string[];
168
+ /** Optional human-readable hint about obtaining credentials. */
169
+ credentials?: string;
170
+ }
171
+ interface AgentCard {
172
+ name: string;
173
+ description: string;
174
+ /** JSON-RPC endpoint URL — clients POST methods here. */
175
+ url: string;
176
+ version: string;
177
+ documentationUrl?: string;
178
+ provider?: AgentProvider;
179
+ capabilities: AgentCapabilities;
180
+ authentication: AgentCardAuthentication;
181
+ defaultInputModes: string[];
182
+ defaultOutputModes: string[];
183
+ skills: AgentSkill[];
184
+ }
185
+
186
+ /**
187
+ * Task persistence behind the JSON-RPC dispatcher. Default adapter is in
188
+ * memory with a 1-hour TTL — adequate for tests, scratch, and Workers with
189
+ * a short-lived process. Production deployments wire their own
190
+ * `TaskStore` (D1, postgres, Durable Object) via `GatewayConfig.a2a`.
191
+ */
192
+
193
+ interface TaskStore {
194
+ get(id: string): Promise<Task | undefined>;
195
+ put(task: Task): Promise<void>;
196
+ delete(id: string): Promise<void>;
197
+ }
198
+ declare class InMemoryTaskStore implements TaskStore {
199
+ private readonly ttlMs;
200
+ private readonly entries;
201
+ constructor(ttlMs?: number);
202
+ get(id: string): Promise<Task | undefined>;
203
+ put(task: Task): Promise<void>;
204
+ delete(id: string): Promise<void>;
205
+ /**
206
+ * Sweep expired tasks. Called inline on every read/write — cheap for the
207
+ * Map sizes this is designed for (10s–1000s of concurrent tasks).
208
+ */
209
+ private gc;
210
+ }
211
+
212
+ /**
213
+ * @stable
214
+ *
215
+ * Durable `TaskStore` against any SQL store. Adapter-agnostic: callers wire a
216
+ * `SqlAdapter` against their driver (D1, postgres, sqlite, libSQL, Turso) and
217
+ * the same store survives gateway restarts so an in-flight task (and its
218
+ * artifacts) is recoverable after a Worker recycle.
219
+ *
220
+ * Schema is one table: tasks keyed by id with the full JSON payload, plus a
221
+ * secondary index on `context_id` so `tasks/resubscribe` and conversational
222
+ * lookups by context are O(log n). TTL is enforced at read time the same way
223
+ * `InMemoryTaskStore` does — the gateway is single-writer per task id so a
224
+ * stale row is invisible to callers regardless of when the row is physically
225
+ * deleted.
226
+ *
227
+ * Why not bake in a specific driver? Hono workers run on Cloudflare (D1),
228
+ * Node (pg / sqlite), Bun, Deno. Burning a hard dependency on one client
229
+ * limits the gateway's reach. The adapter indirection costs ~5 lines per
230
+ * driver in the consumer's code and keeps the package free of native deps.
231
+ *
232
+ * @example D1
233
+ * import { SqlTaskStore, d1ToSqlAdapter } from '@tangle-network/agent-gateway'
234
+ * const store = new SqlTaskStore(d1ToSqlAdapter(env.DB))
235
+ * await store.migrate()
236
+ * const gw = createAgentGateway({ ..., a2a: { taskStore: store } })
237
+ *
238
+ * @example libSQL / Turso
239
+ * import { createClient } from '@libsql/client'
240
+ * const client = createClient({ url: process.env.TURSO_URL!, authToken: process.env.TURSO_TOKEN! })
241
+ * const libsql: SqlAdapter = {
242
+ * exec: async (sql, params = []) => {
243
+ * const r = await client.execute({ sql, args: params as never[] })
244
+ * return { rowsAffected: Number(r.rowsAffected ?? 0) }
245
+ * },
246
+ * query: async (sql, params = []) => {
247
+ * const r = await client.execute({ sql, args: params as never[] })
248
+ * return r.rows as unknown as Record<string, unknown>[]
249
+ * },
250
+ * }
251
+ * const store = new SqlTaskStore(libsql)
252
+ * await store.migrate()
253
+ */
254
+
255
+ /**
256
+ * Minimal SQL driver shape — identical to agent-runtime's `SqlAdapter` so the
257
+ * same wrapper code works for both packages. Parameter placeholders MUST be
258
+ * `?` (positional); driver wrappers that use `$1`, `$2`, … should rewrite at
259
+ * the adapter boundary (see node-postgres example in the durability docs).
260
+ */
261
+ interface SqlAdapter {
262
+ exec(sql: string, params?: readonly unknown[]): Promise<{
263
+ rowsAffected: number;
264
+ }>;
265
+ query<TRow = Record<string, unknown>>(sql: string, params?: readonly unknown[]): Promise<TRow[]>;
266
+ }
267
+ /**
268
+ * Adapt a Cloudflare D1 binding to `SqlAdapter`. The package never imports
269
+ * `@cloudflare/workers-types`; the binding's structural shape lines up via
270
+ * TypeScript structural compatibility.
271
+ */
272
+ declare function d1ToSqlAdapter(db: D1DatabaseLike): SqlAdapter;
273
+ interface D1DatabaseLike {
274
+ prepare(sql: string): D1StmtLike;
275
+ }
276
+ interface D1StmtLike {
277
+ bind(...params: unknown[]): D1StmtLike;
278
+ run(): Promise<unknown>;
279
+ all<TRow = unknown>(): Promise<{
280
+ results?: TRow[];
281
+ }>;
282
+ }
283
+ /**
284
+ * SQL-backed TaskStore. Stores the full Task JSON; reads return a deep clone
285
+ * so callers never observe shared references. TTL is enforced at read time:
286
+ * expired rows are filtered out and (best-effort) deleted, matching the
287
+ * in-memory store's semantics so behavior is portable across both adapters.
288
+ */
289
+ declare class SqlTaskStore implements TaskStore {
290
+ private readonly db;
291
+ private readonly opts;
292
+ constructor(db: SqlAdapter, opts?: {
293
+ ttlMs?: number;
294
+ table?: string;
295
+ });
296
+ private get ttlMs();
297
+ private get table();
298
+ /** Idempotent. Call once at deploy. */
299
+ migrate(): Promise<void>;
300
+ get(id: string): Promise<Task | undefined>;
301
+ put(task: Task): Promise<void>;
302
+ delete(id: string): Promise<void>;
303
+ /**
304
+ * Lookup tasks by contextId — used by `tasks/resubscribe` and the multi-turn
305
+ * dispatcher. Returns most-recent-first. Not part of the base TaskStore
306
+ * interface since the in-memory store doesn't expose it; consumers that
307
+ * specifically wire SqlTaskStore can use it for richer queries.
308
+ */
309
+ listByContext(contextId: string): Promise<Task[]>;
310
+ }
311
+
312
+ /**
313
+ * @stable
314
+ *
315
+ * A2A push notifications — a webhook delivery channel that fires when a task
316
+ * reaches a terminal state (`completed`, `canceled`, `failed`, `rejected`).
317
+ * The protocol specifies four JSON-RPC methods (`tasks/pushNotificationConfig/`
318
+ * {set, get, list, delete}) for registering / inspecting / removing configs,
319
+ * plus the delivery contract: an HTTP POST to the registered URL with the
320
+ * task envelope as the body and an HMAC-SHA256 signature for verification.
321
+ *
322
+ * This is the minimum shape long-horizon agents need. A consumer that finishes
323
+ * a task in 30 minutes can't keep an SSE stream open against a Worker (CPU
324
+ * limits) or an unauthenticated browser tab (network drops) — they need a
325
+ * fire-and-forget endpoint the gateway calls when the task is done.
326
+ *
327
+ * Out of scope for the first pass: retries, queue durability, partial-state
328
+ * notifications. If the webhook returns non-2xx or the request fails, the
329
+ * gateway logs and moves on — the consumer's endpoint should idempotently
330
+ * pull state via `tasks/get` rather than rely on at-least-once delivery.
331
+ *
332
+ * @example registering a webhook
333
+ * {
334
+ * "jsonrpc": "2.0", "id": 1, "method": "tasks/pushNotificationConfig/set",
335
+ * "params": {
336
+ * "taskId": "task_abc",
337
+ * "pushNotificationConfig": {
338
+ * "id": "cfg_1",
339
+ * "url": "https://my-consumer.example.com/agent/done",
340
+ * "token": "my-shared-secret-not-the-hmac-secret"
341
+ * }
342
+ * }
343
+ * }
344
+ *
345
+ * @example webhook delivery
346
+ * POST https://my-consumer.example.com/agent/done
347
+ * X-A2A-Notification-Token: my-shared-secret-not-the-hmac-secret
348
+ * X-A2A-Signature: sha256=<hex(HMAC-SHA256(webhookSecret, body))>
349
+ * Content-Type: application/json
350
+ * { "taskId": "task_abc", "state": "completed", "task": { ...full Task... } }
351
+ */
352
+
353
+ /**
354
+ * Authentication metadata for the webhook itself. The A2A spec leaves this
355
+ * to consumers — the most common shape is a bearer token the gateway sends as
356
+ * `Authorization: <scheme> <credential>`. We pass it through verbatim.
357
+ */
358
+ interface PushNotificationAuthentication {
359
+ schemes: string[];
360
+ credentials?: string;
361
+ }
362
+ /**
363
+ * Per-task push notification configuration. A task can have multiple configs
364
+ * (e.g. one for the consumer's own webhook + one for an audit log endpoint).
365
+ */
366
+ interface PushNotificationConfig {
367
+ /** Stable id within the task's config set. Required for get/delete addressing. */
368
+ id: string;
369
+ /** HTTPS URL the gateway will POST to. */
370
+ url: string;
371
+ /**
372
+ * Opaque token the gateway sends back as `X-A2A-Notification-Token` so the
373
+ * webhook can verify the call originated from a registration the consumer
374
+ * authorized. Distinct from the HMAC signature (which proves the body
375
+ * wasn't tampered with) — this proves the registration is recognised.
376
+ */
377
+ token?: string;
378
+ /** Optional webhook-side auth metadata. */
379
+ authentication?: PushNotificationAuthentication;
380
+ }
381
+ interface TaskPushNotificationConfig {
382
+ taskId: string;
383
+ pushNotificationConfig: PushNotificationConfig;
384
+ }
385
+ /**
386
+ * Storage for push configs. The default in-memory store is fine for single
387
+ * Worker instances + tests; production multi-instance deployments need
388
+ * `SqlPushNotificationStore` (or any other shared-state adapter) so a config
389
+ * registered on instance A is visible to a delivery firing from instance B.
390
+ */
391
+ interface PushNotificationStore {
392
+ set(taskId: string, config: PushNotificationConfig): Promise<void>;
393
+ get(taskId: string, configId: string): Promise<PushNotificationConfig | undefined>;
394
+ list(taskId: string): Promise<PushNotificationConfig[]>;
395
+ delete(taskId: string, configId: string): Promise<void>;
396
+ }
397
+ declare class InMemoryPushNotificationStore implements PushNotificationStore {
398
+ private readonly byTask;
399
+ set(taskId: string, config: PushNotificationConfig): Promise<void>;
400
+ get(taskId: string, configId: string): Promise<PushNotificationConfig | undefined>;
401
+ list(taskId: string): Promise<PushNotificationConfig[]>;
402
+ delete(taskId: string, configId: string): Promise<void>;
403
+ }
404
+ /** SQL-backed push config store. Schema: one row per (taskId, configId). */
405
+ declare class SqlPushNotificationStore implements PushNotificationStore {
406
+ private readonly db;
407
+ private readonly table;
408
+ constructor(db: SqlAdapter, table?: string);
409
+ migrate(): Promise<void>;
410
+ set(taskId: string, config: PushNotificationConfig): Promise<void>;
411
+ get(taskId: string, configId: string): Promise<PushNotificationConfig | undefined>;
412
+ list(taskId: string): Promise<PushNotificationConfig[]>;
413
+ delete(taskId: string, configId: string): Promise<void>;
414
+ }
415
+ /**
416
+ * Send the webhook for each registered config on a task. Signs the body with
417
+ * HMAC-SHA256 against `webhookSecret` so the consumer can verify authenticity.
418
+ * Fire-and-forget per the design note above — the function awaits delivery
419
+ * (so observability hooks see the result) but does not retry on failure.
420
+ *
421
+ * The caller decides *when* to deliver — typically on terminal-state
422
+ * transitions emitted from `message/send` and `message/stream`.
423
+ */
424
+ declare function deliverPushNotifications(args: {
425
+ task: Task;
426
+ store: PushNotificationStore;
427
+ webhookSecret: string | undefined;
428
+ /** Inject for tests. Defaults to global `fetch`. */
429
+ fetcher?: typeof fetch;
430
+ /** Optional callback so the gateway's observer can log delivery outcomes. */
431
+ onDelivery?: (result: PushDeliveryResult) => void;
432
+ }): Promise<PushDeliveryResult[]>;
433
+ interface PushDeliveryResult {
434
+ taskId: string;
435
+ configId: string;
436
+ url: string;
437
+ ok: boolean;
438
+ status?: number;
439
+ error?: string;
440
+ }
441
+
442
+ /**
443
+ * Observability hook surface.
444
+ *
445
+ * Consumers implement GatewayObserver to wire the gateway into their existing
446
+ * telemetry stack (Langfuse, OTEL, structured logs, Prometheus, etc.) without
447
+ * the gateway itself depending on any of those libraries.
448
+ *
449
+ * Every event carries a requestId so downstream metrics can correlate the
450
+ * payment verification, sandbox execution, and settlement for one request.
451
+ * When no observer is configured, the gateway stays silent.
452
+ */
453
+
454
+ interface RequestContext {
455
+ requestId: string;
456
+ agentSlug: string;
457
+ startMs: number;
458
+ }
459
+ interface AuthFailureReason {
460
+ method: 'x402' | 'mpp' | 'apikey' | 'none';
461
+ code: string;
462
+ httpStatus: number;
463
+ }
464
+ interface GatewayObserver {
465
+ /** Called at the start of every chat completions POST. */
466
+ onRequestStart?: (ctx: RequestContext) => void | Promise<void>;
467
+ /** Called when a payment method has been successfully verified. */
468
+ onPaymentVerified?: (ctx: RequestContext, info: {
469
+ method: PaymentMethod;
470
+ consumerId: string;
471
+ keyId?: string;
472
+ }) => void | Promise<void>;
473
+ /** Called when auth fails — every branch. */
474
+ onAuthFailure?: (ctx: RequestContext, reason: AuthFailureReason) => void | Promise<void>;
475
+ /** Called when a consumer hits the rate limit. */
476
+ onRateLimited?: (ctx: RequestContext, info: {
477
+ consumerId: string;
478
+ retryAfterSeconds: number;
479
+ }) => void | Promise<void>;
480
+ /** Called when the request body exceeds the 64KB limit. */
481
+ onBodyTooLarge?: (ctx: RequestContext, contentLength: number) => void | Promise<void>;
482
+ /**
483
+ * Called when prompt-injection patterns are detected.
484
+ * `blocked` is true when blockInjection config is on and the request was
485
+ * rejected; false when the patterns were logged but the request proceeded.
486
+ */
487
+ onInjectionDetected?: (ctx: RequestContext, info: {
488
+ consumerId: string;
489
+ patterns: string[];
490
+ blocked: boolean;
491
+ }) => void | Promise<void>;
492
+ /** Called after a successful stream completes and recordUsage has fired. */
493
+ onRequestComplete?: (ctx: RequestContext, usage: GatewayUsageEvent) => void | Promise<void>;
494
+ /** Called when the sandbox throws. The error message is pre-scrubbed. */
495
+ onStreamError?: (ctx: RequestContext, info: {
496
+ consumerId: string;
497
+ errorMessage: string;
498
+ }) => void | Promise<void>;
499
+ /** Called when settlement fails. Payment already occurred; this is async bookkeeping. */
500
+ onSettlementError?: (ctx: RequestContext, info: {
501
+ consumerId: string;
502
+ method: PaymentMethod;
503
+ errorMessage: string;
504
+ }) => void | Promise<void>;
505
+ }
506
+ /**
507
+ * Structured-log observer. Emits one JSON line per event on the `log` function.
508
+ * Default sink: console.log. Production consumers usually pipe their own
509
+ * structured logger (pino, winston, the cf Logs binding).
510
+ *
511
+ * Usage:
512
+ * new ConsoleObserver(({ level, event, ...rest }) => logger.info({ event, ...rest }))
513
+ */
514
+ declare class ConsoleObserver implements GatewayObserver {
515
+ private readonly log;
516
+ constructor(log?: (entry: Record<string, unknown>) => void);
517
+ private emit;
518
+ onRequestStart(ctx: RequestContext): void;
519
+ onPaymentVerified(ctx: RequestContext, info: {
520
+ method: PaymentMethod;
521
+ consumerId: string;
522
+ keyId?: string;
523
+ }): void;
524
+ onAuthFailure(ctx: RequestContext, reason: AuthFailureReason): void;
525
+ onRateLimited(ctx: RequestContext, info: {
526
+ consumerId: string;
527
+ retryAfterSeconds: number;
528
+ }): void;
529
+ onBodyTooLarge(ctx: RequestContext, contentLength: number): void;
530
+ onInjectionDetected(ctx: RequestContext, info: {
531
+ consumerId: string;
532
+ patterns: string[];
533
+ blocked: boolean;
534
+ }): void;
535
+ onRequestComplete(ctx: RequestContext, usage: GatewayUsageEvent): void;
536
+ onStreamError(ctx: RequestContext, info: {
537
+ consumerId: string;
538
+ errorMessage: string;
539
+ }): void;
540
+ onSettlementError(ctx: RequestContext, info: {
541
+ consumerId: string;
542
+ method: PaymentMethod;
543
+ errorMessage: string;
544
+ }): void;
545
+ }
546
+ /**
547
+ * Compose multiple observers into one. Errors in any individual observer
548
+ * don't break the others (fire-and-forget telemetry).
549
+ */
550
+ declare class CompositeObserver implements GatewayObserver {
551
+ private readonly observers;
552
+ constructor(observers: GatewayObserver[]);
553
+ private fanOut;
554
+ onRequestStart: (ctx: RequestContext) => Promise<void>;
555
+ onPaymentVerified: (ctx: RequestContext, info: Parameters<Required<GatewayObserver>["onPaymentVerified"]>[1]) => Promise<void>;
556
+ onAuthFailure: (ctx: RequestContext, reason: AuthFailureReason) => Promise<void>;
557
+ onRateLimited: (ctx: RequestContext, info: Parameters<Required<GatewayObserver>["onRateLimited"]>[1]) => Promise<void>;
558
+ onBodyTooLarge: (ctx: RequestContext, contentLength: number) => Promise<void>;
559
+ onInjectionDetected: (ctx: RequestContext, info: Parameters<Required<GatewayObserver>["onInjectionDetected"]>[1]) => Promise<void>;
560
+ onRequestComplete: (ctx: RequestContext, usage: GatewayUsageEvent) => Promise<void>;
561
+ onStreamError: (ctx: RequestContext, info: Parameters<Required<GatewayObserver>["onStreamError"]>[1]) => Promise<void>;
562
+ onSettlementError: (ctx: RequestContext, info: Parameters<Required<GatewayObserver>["onSettlementError"]>[1]) => Promise<void>;
563
+ }
564
+ /**
565
+ * Generate a request-id. Crypto-random 16 bytes, hex-encoded with an `req_` prefix.
566
+ * Works in Workers, Node, and browsers — all have globalThis.crypto.
567
+ */
568
+ declare function generateRequestId(): string;
569
+
570
+ interface AgentMeta {
571
+ /** Unique agent identifier (workspace ID, session ID, etc.) */
572
+ id: string;
573
+ /** Owner/creator user ID */
574
+ ownerId: string;
575
+ /** Public URL slug */
576
+ slug: string;
577
+ /** System prompt for the agent (injected before consumer messages) */
578
+ systemPrompt?: string;
579
+ /** Per-token price in USD (default: 0.00002) */
580
+ pricePerTokenUsd: number;
581
+ /** Platform fee as decimal 0-1 (default: 0.20 = 20%) */
582
+ platformFeePercent: number;
583
+ /** Remote operator endpoint for sovereignty mode (null = centralized) */
584
+ sandboxEndpoint: string | null;
585
+ /** Sandbox ID on remote operator */
586
+ remoteSandboxId: string | null;
587
+ /** PASETO bearer token for remote operator auth */
588
+ remoteBearerToken: string | null;
589
+ /** Whether agent is published and accepting requests */
590
+ enabled: boolean;
591
+ /**
592
+ * CLI harness backend that runs this agent inside the sandbox sidecar.
593
+ *
594
+ * When set, the host's `getSandbox()` SHOULD return a `SandboxBox`
595
+ * whose `streamPrompt` POSTs to the sidecar's
596
+ * `POST /agent/invoke/chat/completions` endpoint with
597
+ * `model: "<harness>/<harnessModel>"` — that endpoint runs the
598
+ * harness against the sandbox workspace and streams OpenAI-shape
599
+ * `chat.completion.chunk` frames back.
600
+ *
601
+ * When unset (legacy / template mode), the host's `streamPrompt`
602
+ * falls back to the template's own `/api/chat/completions` (proxied
603
+ * via the sidecar's `/agent/invoke`).
604
+ *
605
+ * Known harnesses (registered in agent-dev-container's
606
+ * cli-agent-bindings.ts): opencode, claude-code, codex, kimi-code,
607
+ * amp, factory-droids, pi, hermes, openclaw, forge, acp, cursor.
608
+ * Aliases the sidecar canonicalizes: claude → claude-code,
609
+ * kimi → kimi-code, factory → factory-droids.
610
+ */
611
+ harness?: string;
612
+ /**
613
+ * Model identifier to pass after the harness in the
614
+ * `<harness>/<model>` slash form. Format is harness-specific:
615
+ * claude-code: "sonnet", "opus", or a versioned id like
616
+ * "claude-sonnet-4-20250514"
617
+ * opencode: "anthropic/claude-sonnet-4-5", "openai/gpt-4o", …
618
+ * (opencode embeds provider before model)
619
+ * codex: "gpt-5-codex"
620
+ * kimi-code: "kimi-for-coding"
621
+ *
622
+ * Only meaningful when `harness` is set; ignored otherwise.
623
+ */
624
+ harnessModel?: string;
625
+ /**
626
+ * Optional human description surfaced in the A2A Agent Card. Defaults to
627
+ * `"{slug} agent"` when absent.
628
+ */
629
+ description?: string;
630
+ /**
631
+ * Optional A2A skill descriptors. Each entry advertises what the agent
632
+ * can do so non-Tangle A2A clients can select agents by capability. When
633
+ * absent, the gateway synthesizes a single default `chat` skill from
634
+ * `slug` + `description`.
635
+ */
636
+ skills?: AgentSkill[];
637
+ }
638
+ type PaymentMethod = 'x402' | 'mpp' | 'apikey' | 'none';
639
+ interface X402Config {
640
+ /** Ethereum operator address for SpendAuth verification */
641
+ operatorAddress: string;
642
+ /** Blockchain network ID (default: 3799) */
643
+ chainId: number;
644
+ /** ShieldedCredits contract address */
645
+ creditsAddress?: string;
646
+ /** RPC URL for on-chain verification (optional, demo mode skips this) */
647
+ rpcUrl?: string;
648
+ /** Demo mode: skip signature verification (default: false). NEVER enable in production. */
649
+ demoMode?: boolean;
650
+ /** Production signer verification. Called with the raw SpendAuth payload. Return true if signature is valid. */
651
+ verifySigner?: (payload: Record<string, unknown>) => Promise<boolean>;
652
+ }
653
+ interface MppConfig {
654
+ /** MPP realm (e.g. "agents.tangle.tools") */
655
+ realm: string;
656
+ /** MPP method name (default: "blueprintevm") */
657
+ method?: string;
658
+ }
659
+ interface PaymentResult {
660
+ method: PaymentMethod;
661
+ consumerId: string;
662
+ /**
663
+ * Per-request id (matches `RequestContext.requestId` from the
664
+ * Observer pattern). Threaded into `settlePayment` so callers can
665
+ * attribute revenue deterministically per-request without scanning
666
+ * a FIFO queue keyed by consumerId — when the same consumer is
667
+ * paying for two concurrent requests against agents A and B, FIFO
668
+ * misroutes one. With `requestId` the call site can write a
669
+ * settlement row keyed exactly to the request that earned it.
670
+ */
671
+ requestId: string;
672
+ }
673
+ interface ApiKeyInfo {
674
+ keyId: string;
675
+ consumerId: string;
676
+ /** Scopes this key is authorized for (e.g. ["chat", "forms"]) */
677
+ scopes?: string[];
678
+ /** Per-key rate limit override (requests per minute). If set, overrides global rate limit. */
679
+ rateLimitPerMinute?: number;
680
+ /** Per-key daily limit override. */
681
+ dailyLimit?: number;
682
+ }
683
+ interface GatewayUsageEvent {
684
+ /**
685
+ * Per-request id (matches `RequestContext.requestId`). Lets
686
+ * `recordUsage` correlate the usage row to the same request that
687
+ * `settlePayment` settles, observability hooks observe, and
688
+ * `onRequestComplete` reports — without re-deriving from a
689
+ * synthetic key. Required field as of 0.4.0; the gateway always has
690
+ * it in scope at the recordUsage call site.
691
+ */
692
+ requestId: string;
693
+ agentId: string;
694
+ agentSlug: string;
695
+ consumerId: string;
696
+ paymentMethod: PaymentMethod;
697
+ inputTokens: number;
698
+ outputTokens: number;
699
+ totalCostUsd: number;
700
+ ownerEarnedUsd: number;
701
+ platformFeeUsd: number;
702
+ durationMs: number;
703
+ }
704
+ interface SandboxStreamEvent {
705
+ type?: string;
706
+ data?: {
707
+ part?: {
708
+ type?: string;
709
+ text?: string;
710
+ };
711
+ delta?: string;
712
+ finalText?: string;
713
+ /**
714
+ * Optional sandbox-side signal that the agent has paused and is waiting
715
+ * for additional input from the caller. The A2A gateway translates this
716
+ * into an `input-required` task status; the caller can then submit a
717
+ * follow-up `message/send` with the same `taskId` to continue. Ignored
718
+ * by the OpenAI-compat path. Carry an optional `prompt` to surface to
719
+ * the caller (rendered as the input-required message body).
720
+ */
721
+ inputRequired?: {
722
+ prompt?: string;
723
+ };
724
+ };
725
+ }
726
+ interface SandboxBox {
727
+ streamPrompt(message: string, opts?: {
728
+ sessionId?: string;
729
+ systemPrompt?: string;
730
+ }): AsyncIterable<SandboxStreamEvent>;
731
+ }
732
+ interface GatewayConfig {
733
+ /** Resolve agent metadata by slug. Return null if not found or not published. */
734
+ resolveAgent: (slug: string) => Promise<AgentMeta | null>;
735
+ /** Get a sandbox instance for the agent. Called after payment is verified. */
736
+ getSandbox: (agent: AgentMeta) => Promise<SandboxBox>;
737
+ /**
738
+ * Optional host authorization hook fired after payment verification
739
+ * and before sandbox resolution. Use it for per-agent allowlists,
740
+ * per-consumer quotas, contract scope checks, and instance ownership.
741
+ */
742
+ authorizeConsumer?: (agent: AgentMeta, consumer: {
743
+ method: PaymentMethod;
744
+ consumerId: string;
745
+ keyId?: string;
746
+ requestId: string;
747
+ }) => Promise<{
748
+ allow: true;
749
+ } | {
750
+ allow: false;
751
+ reason: string;
752
+ code: string;
753
+ }>;
754
+ /** Record a usage event after request completes. */
755
+ recordUsage: (event: GatewayUsageEvent) => Promise<void>;
756
+ /** x402 payment configuration */
757
+ x402: X402Config;
758
+ /** MPP (Machine Payments Protocol) configuration. If provided, gateway accepts Authorization: Payment headers. */
759
+ mpp?: MppConfig;
760
+ /**
761
+ * Verify an API key. Return key info if valid, null if invalid.
762
+ * Default: accepts any `sk_agent_*` key (demo mode).
763
+ */
764
+ verifyApiKey?: (authHeader: string) => Promise<ApiKeyInfo | null>;
765
+ /**
766
+ * Settle payment after successful response.
767
+ * For x402: call ShieldedCredits.claimPayment()
768
+ * For API key: deduct from spending limit
769
+ * Default: no-op (demo mode).
770
+ */
771
+ settlePayment?: (payment: PaymentResult, cost: number) => Promise<void>;
772
+ /** Base URL for API key purchase links (e.g. "https://film.tangle.tools") */
773
+ baseUrl?: string;
774
+ /** Max message length in chars (default: 8000) */
775
+ maxMessageLength?: number;
776
+ /** Required scope for chat endpoint (default: "chat"). API keys must include this scope. */
777
+ requiredScope?: string;
778
+ /** Block requests with detected injection patterns (default: false — log only) */
779
+ blockInjection?: boolean;
780
+ /** Rate limiting config. Default: 60 requests per 60 seconds per consumer. */
781
+ rateLimit?: {
782
+ limit: number;
783
+ windowSeconds: number;
784
+ };
785
+ /** Custom rate limit store (default: in-memory). Use KV-backed for Workers. */
786
+ rateLimitStore?: RateLimitStore;
787
+ /** Nonce replay protection store (default: in-memory). Rejects reused x402 nonces. */
788
+ nonceStore?: NonceStore;
789
+ /**
790
+ * Observability hook. When set, the gateway emits typed events for request
791
+ * lifecycle, auth outcomes, rate limits, injection detection, usage, errors,
792
+ * and settlement failures. See ./observer.ts for the interface and
793
+ * ConsoleObserver / CompositeObserver implementations.
794
+ */
795
+ observer?: GatewayObserver;
796
+ /**
797
+ * A2A protocol configuration. When set, the gateway exposes the A2A
798
+ * surface alongside its OpenAI-compatible endpoints:
799
+ * GET /:slug/.well-known/agent.json — AgentCard discovery
800
+ * POST /:slug — JSON-RPC 2.0 endpoint
801
+ * methods: message/send, message/stream, tasks/get, tasks/cancel
802
+ * Auth + rate-limit + injection-filter + authorization all share the
803
+ * same pipeline as the OpenAI-compat path. `taskStore` defaults to
804
+ * `InMemoryTaskStore`; swap in D1/postgres/DO for durable deployments.
805
+ */
806
+ a2a?: {
807
+ /**
808
+ * Where tasks live. Defaults to `InMemoryTaskStore`; swap in
809
+ * `SqlTaskStore` (D1, postgres, sqlite, libSQL) for durability across
810
+ * gateway restarts.
811
+ */
812
+ taskStore?: TaskStore;
813
+ /**
814
+ * Where push notification configs live. When set, the gateway advertises
815
+ * `capabilities.pushNotifications: true` and exposes the four
816
+ * `tasks/pushNotificationConfig/*` JSON-RPC methods. Defaults to
817
+ * undefined (push support disabled), so the agent card honestly reflects
818
+ * what the gateway will actually do.
819
+ */
820
+ pushStore?: PushNotificationStore;
821
+ /**
822
+ * Shared HMAC secret used to sign webhook deliveries (`X-A2A-Signature:
823
+ * sha256=<hex>`). The consumer's webhook verifies the body against this
824
+ * secret to confirm the call originated from this gateway. Required when
825
+ * `pushStore` is set; without it, deliveries fire unsigned and a
826
+ * malicious party that knows the webhook URL can forge deliveries.
827
+ */
828
+ webhookSecret?: string;
829
+ /**
830
+ * Optional fetcher override for webhook delivery. Defaults to global
831
+ * `fetch`. Override for tests or to wire a queue-backed sender.
832
+ */
833
+ pushFetcher?: typeof fetch;
834
+ };
835
+ }
836
+ interface ChatMessage {
837
+ role: 'system' | 'user' | 'assistant' | 'tool';
838
+ content: string;
839
+ }
840
+ interface ChatCompletionRequest {
841
+ model?: string;
842
+ messages: ChatMessage[];
843
+ stream?: boolean;
844
+ temperature?: number;
845
+ max_tokens?: number;
846
+ }
847
+ interface ChatCompletionChunk {
848
+ id: string;
849
+ object: 'chat.completion.chunk';
850
+ created: number;
851
+ model: string;
852
+ choices: Array<{
853
+ index: number;
854
+ delta: {
855
+ content?: string;
856
+ role?: string;
857
+ };
858
+ finish_reason: string | null;
859
+ }>;
860
+ }
861
+
862
+ export { type TaskStatusUpdateEvent as $, type ApiKeyInfo as A, type PushNotificationAuthentication as B, type ChatMessage as C, type D1DatabaseLike as D, type PushNotificationConfig as E, type FilePart as F, type GatewayConfig as G, type PushNotificationStore as H, InMemoryPushNotificationStore as I, type JSONRPCErrorResponse as J, type SandboxStreamEvent as K, type SqlAdapter as L, type MppConfig as M, SqlPushNotificationStore as N, SqlTaskStore as O, type Part as P, type StreamingEvent as Q, type RequestContext as R, type SandboxBox as S, type Task as T, type TaskArtifactUpdateEvent as U, type TaskIdParams as V, type TaskPushNotificationConfig as W, type X402Config as X, type TaskPushNotificationConfigGetParams as Y, type TaskState as Z, type TaskStatus as _, A2A_ERROR_CODES as a, type TaskStore as a0, type TextPart as a1, d1ToSqlAdapter as a2, deliverPushNotifications as a3, generateRequestId as a4, type AgentCapabilities as b, type AgentCard as c, type AgentCardAuthentication as d, type AgentMeta as e, type AgentProvider as f, type AgentSkill as g, type Artifact as h, type AuthFailureReason as i, type ChatCompletionChunk as j, type ChatCompletionRequest as k, CompositeObserver as l, ConsoleObserver as m, type D1StmtLike as n, type DataPart as o, type GatewayObserver as p, type GatewayUsageEvent as q, InMemoryTaskStore as r, type JSONRPCRequest as s, type JSONRPCResponse as t, type JSONRPCSuccessResponse as u, type Message as v, type MessageSendParams as w, type PaymentMethod as x, type PaymentResult as y, type PushDeliveryResult as z };