oacp-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js ADDED
@@ -0,0 +1,715 @@
1
+ /**
2
+ * OACP SDK — OACPAgent Class
3
+ *
4
+ * Main entry point for agents using the OACP protocol.
5
+ * Handles registration, heartbeat, and discovery.
6
+ *
7
+ * Usage:
8
+ * const agent = new OACPAgent({
9
+ * name: 'my-agent',
10
+ * capabilities: ['research-summarization'],
11
+ * supabaseUrl: process.env.SUPABASE_URL!,
12
+ * supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
13
+ * });
14
+ * await agent.register();
15
+ * agent.startHeartbeat();
16
+ * const agents = await agent.discover({ capability: 'data-extraction' });
17
+ */
18
+ import { DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_KEYPAIR_PATH, OACP_PUBLIC_REGISTRY_URL, OACP_PUBLIC_REGISTRY_KEY, } from './config.js';
19
+ import { loadOrCreateKeypair, signMessage, verifySignature, canonicalizeMessage, } from './crypto.js';
20
+ import { validatePayload, isSenderAllowed, matchAction, DEFAULT_TRUST_POLICY, } from './trust.js';
21
+ export class OACPAgent {
22
+ config;
23
+ keypair;
24
+ agentId = null;
25
+ registryToken = null;
26
+ heartbeatTimer = null;
27
+ apiBaseUrl;
28
+ registered = false;
29
+ // Messaging — event handlers and Realtime subscription
30
+ // deno-lint-ignore no-explicit-any
31
+ messageHandlers = new Map();
32
+ // deno-lint-ignore no-explicit-any
33
+ realtimeChannel = null;
34
+ // deno-lint-ignore no-explicit-any
35
+ supabaseClient = null;
36
+ // Trust model
37
+ actionSpecs = [];
38
+ trustPolicy;
39
+ // deno-lint-ignore no-explicit-any
40
+ actionHandlers = new Map();
41
+ constructor(config) {
42
+ this.config = config;
43
+ // Load or generate keypair
44
+ const keypairPath = config.keypairPath || DEFAULT_KEYPAIR_PATH;
45
+ this.keypair = loadOrCreateKeypair(keypairPath);
46
+ // Set API base URL
47
+ const supabaseUrl = config.supabaseUrl || OACP_PUBLIC_REGISTRY_URL;
48
+ this.apiBaseUrl =
49
+ config.apiBaseUrl ||
50
+ `${supabaseUrl}/functions/v1/oacp`;
51
+ // Store resolved config for Realtime client
52
+ this.config = {
53
+ ...config,
54
+ supabaseUrl: supabaseUrl,
55
+ supabaseAnonKey: config.supabaseAnonKey || OACP_PUBLIC_REGISTRY_KEY,
56
+ };
57
+ // Trust model
58
+ this.actionSpecs = config.actionSpecs || [];
59
+ this.trustPolicy = config.trustPolicy || DEFAULT_TRUST_POLICY;
60
+ console.log(`[OACP] Agent "${config.name}" initialized (pubkey: ${this.keypair.publicKey.substring(0, 16)}...)`);
61
+ if (this.actionSpecs.length > 0) {
62
+ console.log(`[OACP] Actions: ${this.actionSpecs.map((s) => s.name).join(', ')}`);
63
+ }
64
+ }
65
+ // ============================================================
66
+ // Public Accessors
67
+ // ============================================================
68
+ /** Get the agent's unique ID (available after registration) */
69
+ getAgentId() {
70
+ return this.agentId;
71
+ }
72
+ /** Get the agent's Ed25519 public key (hex) */
73
+ getPublicKey() {
74
+ return this.keypair.publicKey;
75
+ }
76
+ /** Check if the agent is registered */
77
+ isRegistered() {
78
+ return this.registered;
79
+ }
80
+ /** Get the API base URL */
81
+ getApiBaseUrl() {
82
+ return this.apiBaseUrl;
83
+ }
84
+ // ============================================================
85
+ // Registration
86
+ // ============================================================
87
+ /**
88
+ * Register this agent with the OACP Registry.
89
+ *
90
+ * On first call: creates a new registration and stores the agent_id + token.
91
+ * On subsequent calls: re-registers (updates) the existing agent card.
92
+ *
93
+ * @returns The agent_id assigned by the registry
94
+ */
95
+ async register() {
96
+ const body = {
97
+ name: this.config.name,
98
+ capabilities: this.config.capabilities,
99
+ public_key: this.keypair.publicKey,
100
+ };
101
+ if (this.config.ownerHandle)
102
+ body.owner_handle = this.config.ownerHandle;
103
+ if (this.config.description)
104
+ body.description = this.config.description;
105
+ if (this.config.endpointUrl)
106
+ body.endpoint_url = this.config.endpointUrl;
107
+ if (this.config.protocolsSupported)
108
+ body.protocols_supported = this.config.protocolsSupported;
109
+ if (this.config.metadata)
110
+ body.metadata = this.config.metadata;
111
+ // Include structured metadata so other agents can discover our capabilities
112
+ const structuredMeta = {
113
+ ...(body.metadata || {}),
114
+ };
115
+ if (this.actionSpecs.length > 0) {
116
+ structuredMeta.action_specs = this.actionSpecs.map((s) => ({
117
+ name: s.name,
118
+ description: s.description,
119
+ price: s.price || 0,
120
+ }));
121
+ }
122
+ if (this.config.model) {
123
+ structuredMeta.model = this.config.model;
124
+ }
125
+ if (this.config.languages) {
126
+ structuredMeta.languages = this.config.languages;
127
+ }
128
+ if (Object.keys(structuredMeta).length > 0) {
129
+ body.metadata = structuredMeta;
130
+ }
131
+ // Re-registration: include agent_id and auth token
132
+ if (this.agentId && this.registryToken) {
133
+ body.agent_id = this.agentId;
134
+ }
135
+ const headers = {
136
+ 'Content-Type': 'application/json',
137
+ };
138
+ if (this.registryToken) {
139
+ headers['Authorization'] = `Bearer ${this.registryToken}`;
140
+ }
141
+ const response = await fetch(`${this.apiBaseUrl}/register`, {
142
+ method: 'POST',
143
+ headers,
144
+ body: JSON.stringify(body),
145
+ });
146
+ if (!response.ok) {
147
+ const error = (await response.json());
148
+ throw new Error(`Registration failed (${response.status}): ${error.error}`);
149
+ }
150
+ // deno-lint-ignore no-explicit-any
151
+ const data = (await response.json());
152
+ if ('registry_token' in data) {
153
+ // New registration
154
+ const reg = data;
155
+ this.agentId = reg.agent_id;
156
+ this.registryToken = reg.registry_token;
157
+ this.registered = true;
158
+ console.log(`[OACP] Registered as ${reg.agent_id}`);
159
+ // Start Realtime if handlers were registered before register() was called
160
+ if (this.messageHandlers.size > 0) {
161
+ this.startRealtimeSubscription();
162
+ }
163
+ return reg.agent_id;
164
+ }
165
+ else {
166
+ // Re-registration
167
+ const rereg = data;
168
+ this.registered = true;
169
+ console.log(`[OACP] Re-registered ${rereg.agent_id}`);
170
+ return rereg.agent_id;
171
+ }
172
+ }
173
+ // ============================================================
174
+ // Heartbeat
175
+ // ============================================================
176
+ /**
177
+ * Send a single heartbeat to the registry.
178
+ * Updates last_seen_at and confirms online status.
179
+ */
180
+ async heartbeat() {
181
+ if (!this.agentId || !this.registryToken) {
182
+ throw new Error('Agent must be registered before sending heartbeats');
183
+ }
184
+ const response = await fetch(`${this.apiBaseUrl}/heartbeat`, {
185
+ method: 'POST',
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ Authorization: `Bearer ${this.registryToken}`,
189
+ },
190
+ body: JSON.stringify({ agent_id: this.agentId }),
191
+ });
192
+ if (!response.ok) {
193
+ const error = (await response.json());
194
+ throw new Error(`Heartbeat failed (${response.status}): ${error.error}`);
195
+ }
196
+ return (await response.json());
197
+ }
198
+ /**
199
+ * Start automatic heartbeat at the configured interval.
200
+ * Default: every 30 seconds (matching PRD REQ-HB-01).
201
+ */
202
+ startHeartbeat(intervalMs) {
203
+ if (this.heartbeatTimer) {
204
+ console.warn('[OACP] Heartbeat already running');
205
+ return;
206
+ }
207
+ const interval = intervalMs || this.config.heartbeatIntervalMs || DEFAULT_HEARTBEAT_INTERVAL_MS;
208
+ // Send immediate heartbeat, then schedule recurring
209
+ this.heartbeat().catch((err) => {
210
+ console.error('[OACP] Initial heartbeat failed:', err.message);
211
+ });
212
+ this.heartbeatTimer = setInterval(async () => {
213
+ try {
214
+ await this.heartbeat();
215
+ }
216
+ catch (err) {
217
+ const msg = err instanceof Error ? err.message : 'Unknown error';
218
+ console.error(`[OACP] Heartbeat failed: ${msg}`);
219
+ }
220
+ }, interval);
221
+ console.log(`[OACP] Heartbeat started (interval: ${interval}ms)`);
222
+ }
223
+ /**
224
+ * Stop the automatic heartbeat.
225
+ */
226
+ stopHeartbeat() {
227
+ if (this.heartbeatTimer) {
228
+ clearInterval(this.heartbeatTimer);
229
+ this.heartbeatTimer = null;
230
+ console.log('[OACP] Heartbeat stopped');
231
+ }
232
+ }
233
+ // ============================================================
234
+ // Discovery
235
+ // ============================================================
236
+ /**
237
+ * Discover agents in the registry matching the given criteria.
238
+ *
239
+ * @param query - Filter criteria (capability, status, owner_handle, pagination)
240
+ * @returns Discovery results with agents and pagination info
241
+ */
242
+ async discover(query = {}) {
243
+ const params = new URLSearchParams();
244
+ if (query.capability)
245
+ params.set('capability', query.capability);
246
+ if (query.status)
247
+ params.set('status', query.status);
248
+ if (query.owner_handle)
249
+ params.set('owner_handle', query.owner_handle);
250
+ if (query.page)
251
+ params.set('page', String(query.page));
252
+ if (query.page_size)
253
+ params.set('page_size', String(query.page_size));
254
+ const qs = params.toString();
255
+ const url = `${this.apiBaseUrl}/discover${qs ? '?' + qs : ''}`;
256
+ const response = await fetch(url, {
257
+ method: 'GET',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ },
261
+ });
262
+ if (!response.ok) {
263
+ const error = (await response.json());
264
+ throw new Error(`Discovery failed (${response.status}): ${error.error}`);
265
+ }
266
+ return (await response.json());
267
+ }
268
+ // ============================================================
269
+ // Token Economy
270
+ // ============================================================
271
+ /**
272
+ * Get this agent's token balance.
273
+ */
274
+ async getBalance() {
275
+ if (!this.agentId)
276
+ throw new Error('Agent must be registered');
277
+ const response = await fetch(`${this.apiBaseUrl}/balance?agent_id=${this.agentId}`);
278
+ if (!response.ok)
279
+ return 0;
280
+ // deno-lint-ignore no-explicit-any
281
+ const data = (await response.json());
282
+ return data.balance;
283
+ }
284
+ /**
285
+ * Get this agent's transaction history.
286
+ */
287
+ async getTransactions(options = {}) {
288
+ if (!this.agentId || !this.registryToken)
289
+ throw new Error('Agent must be registered');
290
+ const params = new URLSearchParams();
291
+ params.set('agent_id', this.agentId);
292
+ if (options.limit)
293
+ params.set('limit', String(options.limit));
294
+ if (options.type)
295
+ params.set('type', options.type);
296
+ const response = await fetch(`${this.apiBaseUrl}/transactions?${params.toString()}`, {
297
+ headers: { Authorization: `Bearer ${this.registryToken}` },
298
+ });
299
+ if (!response.ok) {
300
+ const error = (await response.json());
301
+ throw new Error(`Get transactions failed: ${error.error}`);
302
+ }
303
+ // deno-lint-ignore no-explicit-any
304
+ return (await response.json());
305
+ }
306
+ /**
307
+ * Transfer tokens to another agent.
308
+ * @private — used internally for task payments
309
+ */
310
+ async transferTokens(toAgentId, amount, messageId, description) {
311
+ if (!this.agentId || !this.registryToken)
312
+ throw new Error('Agent must be registered');
313
+ const response = await fetch(`${this.apiBaseUrl}/transfer`, {
314
+ method: 'POST',
315
+ headers: {
316
+ 'Content-Type': 'application/json',
317
+ Authorization: `Bearer ${this.registryToken}`,
318
+ },
319
+ body: JSON.stringify({
320
+ from_agent_id: this.agentId,
321
+ to_agent_id: toAgentId,
322
+ amount,
323
+ type: 'task_payment',
324
+ message_id: messageId || null,
325
+ description: description || `Payment for task`,
326
+ }),
327
+ });
328
+ if (!response.ok) {
329
+ const error = (await response.json());
330
+ throw new Error(`Transfer failed: ${error.error}`);
331
+ }
332
+ // deno-lint-ignore no-explicit-any
333
+ const data = (await response.json());
334
+ return data.transaction_id;
335
+ }
336
+ // ============================================================
337
+ // Messaging
338
+ // ============================================================
339
+ /**
340
+ * Send a message to another agent.
341
+ *
342
+ * Composes the OACP Message Envelope, signs it with this agent's
343
+ * Ed25519 key, and POSTs to the registry message bus.
344
+ *
345
+ * @param recipientId - Target agent's agent_id
346
+ * @param messageType - Message type (task_request, task_ack, task_result, task_error)
347
+ * @param payload - Message payload object
348
+ * @param options - Optional correlation_id and sequence
349
+ * @returns The message_id assigned by the registry
350
+ */
351
+ async send(recipientId, messageType, payload, options) {
352
+ if (!this.agentId || !this.registryToken) {
353
+ throw new Error('Agent must be registered before sending messages');
354
+ }
355
+ // Canonicalize and sign
356
+ const canonical = canonicalizeMessage({
357
+ sender_agent_id: this.agentId,
358
+ recipient_agent_id: recipientId,
359
+ message_type: messageType,
360
+ payload,
361
+ correlation_id: options?.correlationId,
362
+ sequence: options?.sequence,
363
+ });
364
+ const signature = signMessage(canonical, this.keypair.secretKey);
365
+ const body = {
366
+ sender_agent_id: this.agentId,
367
+ recipient_agent_id: recipientId,
368
+ message_type: messageType,
369
+ payload,
370
+ signature,
371
+ };
372
+ if (options?.correlationId)
373
+ body.correlation_id = options.correlationId;
374
+ if (options?.sequence !== undefined)
375
+ body.sequence = options.sequence;
376
+ const response = await fetch(`${this.apiBaseUrl}/messages/send`, {
377
+ method: 'POST',
378
+ headers: {
379
+ 'Content-Type': 'application/json',
380
+ Authorization: `Bearer ${this.registryToken}`,
381
+ },
382
+ body: JSON.stringify(body),
383
+ });
384
+ if (!response.ok) {
385
+ const error = (await response.json());
386
+ throw new Error(`Send failed (${response.status}): ${error.error}`);
387
+ }
388
+ // deno-lint-ignore no-explicit-any
389
+ const result = (await response.json());
390
+ console.log(`[OACP] Sent ${messageType} to ${recipientId.substring(0, 8)}... (msg: ${result.message_id})`);
391
+ return result.message_id;
392
+ }
393
+ /**
394
+ * Get messages for this agent from the registry.
395
+ *
396
+ * @param options - Filter options (direction, messageType, correlationId, since, limit)
397
+ */
398
+ async getMessages(options = {}) {
399
+ if (!this.agentId || !this.registryToken) {
400
+ throw new Error('Agent must be registered before retrieving messages');
401
+ }
402
+ const params = new URLSearchParams();
403
+ params.set('agent_id', this.agentId);
404
+ if (options.direction)
405
+ params.set('direction', options.direction);
406
+ if (options.messageType)
407
+ params.set('message_type', options.messageType);
408
+ if (options.correlationId)
409
+ params.set('correlation_id', options.correlationId);
410
+ if (options.since)
411
+ params.set('since', options.since);
412
+ if (options.limit)
413
+ params.set('limit', String(options.limit));
414
+ const response = await fetch(`${this.apiBaseUrl}/messages?${params.toString()}`, {
415
+ method: 'GET',
416
+ headers: {
417
+ 'Content-Type': 'application/json',
418
+ Authorization: `Bearer ${this.registryToken}`,
419
+ },
420
+ });
421
+ if (!response.ok) {
422
+ const error = (await response.json());
423
+ throw new Error(`Get messages failed (${response.status}): ${error.error}`);
424
+ }
425
+ // deno-lint-ignore no-explicit-any
426
+ const result = (await response.json());
427
+ return {
428
+ messages: result.messages,
429
+ total: result.total,
430
+ };
431
+ }
432
+ /**
433
+ * Register an event handler for incoming messages.
434
+ *
435
+ * Uses Supabase Realtime to subscribe to the messages table,
436
+ * filtering for messages addressed to this agent.
437
+ *
438
+ * @param eventType - Message type to listen for, or '*' for all
439
+ * @param handler - Callback receiving the message envelope
440
+ */
441
+ on(eventType, handler) {
442
+ const handlers = this.messageHandlers.get(eventType) || [];
443
+ handlers.push(handler);
444
+ this.messageHandlers.set(eventType, handlers);
445
+ // Start Realtime subscription if not already running
446
+ if (!this.realtimeChannel) {
447
+ this.startRealtimeSubscription();
448
+ }
449
+ }
450
+ /**
451
+ * Remove event handlers for a message type.
452
+ */
453
+ off(eventType) {
454
+ this.messageHandlers.delete(eventType);
455
+ // If no handlers left, unsubscribe from Realtime
456
+ if (this.messageHandlers.size === 0 && this.realtimeChannel) {
457
+ this.stopRealtimeSubscription();
458
+ }
459
+ }
460
+ /**
461
+ * Start Supabase Realtime subscription for incoming messages.
462
+ * Called automatically after register() if handlers are registered.
463
+ * @private
464
+ */
465
+ async startRealtimeSubscription() {
466
+ if (!this.agentId) {
467
+ // Not registered yet — will be called again after register()
468
+ return;
469
+ }
470
+ // Already subscribed
471
+ if (this.realtimeChannel)
472
+ return;
473
+ try {
474
+ // Dynamic import for ESM compatibility
475
+ const { createClient } = await import('@supabase/supabase-js');
476
+ if (!this.supabaseClient) {
477
+ this.supabaseClient = createClient(this.config.supabaseUrl || OACP_PUBLIC_REGISTRY_URL, this.config.supabaseAnonKey || OACP_PUBLIC_REGISTRY_KEY);
478
+ }
479
+ const channelName = `oacp-messages-${this.agentId}`;
480
+ this.realtimeChannel = this.supabaseClient
481
+ .channel(channelName)
482
+ .on('postgres_changes', {
483
+ event: 'INSERT',
484
+ schema: 'public',
485
+ table: 'messages',
486
+ filter: `recipient_agent_id=eq.${this.agentId}`,
487
+ },
488
+ // deno-lint-ignore no-explicit-any
489
+ (payload) => {
490
+ const msg = payload.new;
491
+ this.dispatchMessage(msg);
492
+ })
493
+ .subscribe((status) => {
494
+ if (status === 'SUBSCRIBED') {
495
+ console.log(`[OACP] Realtime subscription active for ${this.agentId}`);
496
+ }
497
+ });
498
+ }
499
+ catch (err) {
500
+ console.warn('[OACP] Supabase Realtime not available — use getMessages() for polling instead', err instanceof Error ? err.message : err);
501
+ }
502
+ }
503
+ /**
504
+ * Stop Realtime subscription.
505
+ * @private
506
+ */
507
+ stopRealtimeSubscription() {
508
+ if (this.realtimeChannel && this.supabaseClient) {
509
+ this.supabaseClient.removeChannel(this.realtimeChannel);
510
+ this.realtimeChannel = null;
511
+ console.log('[OACP] Realtime subscription stopped');
512
+ }
513
+ }
514
+ /**
515
+ * Register an action with a trust-validated handler.
516
+ *
517
+ * This is the preferred way to handle incoming tasks. The handler
518
+ * only runs if:
519
+ * 1. The sender is allowed by the trust policy
520
+ * 2. The message matches a declared action_spec
521
+ * 3. The payload validates against the action's inputSchema
522
+ *
523
+ * @param spec - The action specification
524
+ * @param handler - Async function receiving validated payload and full message.
525
+ * Return a result object to auto-send task_result back.
526
+ *
527
+ * @example
528
+ * agent.registerAction(
529
+ * {
530
+ * name: 'summarize-text',
531
+ * description: 'Summarize a topic',
532
+ * inputSchema: { type: 'object', properties: { task: { type: 'string' } }, required: ['task'] },
533
+ * },
534
+ * async (payload, msg) => {
535
+ * const summary = await doResearch(payload.task);
536
+ * return { summary, word_count: summary.split(' ').length };
537
+ * },
538
+ * );
539
+ */
540
+ registerAction(spec, handler) {
541
+ // Add to action specs if not already present
542
+ if (!this.actionSpecs.find((s) => s.name === spec.name)) {
543
+ this.actionSpecs.push(spec);
544
+ }
545
+ this.actionHandlers.set(spec.name, handler);
546
+ // Ensure we're listening for task_requests
547
+ if (!this.messageHandlers.has('__trust_pipeline')) {
548
+ this.messageHandlers.set('__trust_pipeline', []);
549
+ this.on('task_request', (msg) => {
550
+ this.handleTrustedTask(msg).catch((err) => {
551
+ console.error('[OACP] Trust pipeline error:', err);
552
+ });
553
+ });
554
+ }
555
+ console.log(`[OACP] Registered action: ${spec.name} — "${spec.description}"`);
556
+ }
557
+ /**
558
+ * Process a task_request through the trust pipeline.
559
+ * @private
560
+ */
561
+ async handleTrustedTask(msg) {
562
+ const payload = (msg.payload || {});
563
+ const senderId = msg.sender_agent_id;
564
+ // Step 1: Check sender trust
565
+ const senderCheck = isSenderAllowed(senderId, this.trustPolicy);
566
+ if (!senderCheck.allowed) {
567
+ console.log(`[OACP] ⛔ Rejected task from ${senderId.substring(0, 8)}...: ${senderCheck.reason}`);
568
+ await this.send(senderId, 'task_error', {
569
+ error: 'unauthorized',
570
+ message: senderCheck.reason,
571
+ }, { correlationId: msg.correlation_id });
572
+ return;
573
+ }
574
+ // Step 2: Match action
575
+ const actionMatch = matchAction(payload, this.actionSpecs);
576
+ if (!actionMatch.matched) {
577
+ console.log(`[OACP] ⛔ No matching action: ${actionMatch.reason}`);
578
+ await this.send(senderId, 'task_error', {
579
+ error: 'unknown_action',
580
+ message: actionMatch.reason,
581
+ available_actions: this.actionSpecs.map((s) => ({
582
+ name: s.name,
583
+ description: s.description,
584
+ })),
585
+ }, { correlationId: msg.correlation_id });
586
+ return;
587
+ }
588
+ const action = actionMatch.matched;
589
+ // Step 3: Validate payload against inputSchema
590
+ const validation = validatePayload(payload, action.inputSchema);
591
+ if (!validation.valid) {
592
+ console.log(`[OACP] ⛔ Payload validation failed: ${validation.errors.join('; ')}`);
593
+ await this.send(senderId, 'task_error', {
594
+ error: 'invalid_payload',
595
+ message: `Payload does not match schema for action "${action.name}"`,
596
+ validation_errors: validation.errors,
597
+ }, { correlationId: msg.correlation_id });
598
+ return;
599
+ }
600
+ // Step 4: Execute handler
601
+ console.log(`[OACP] ✅ Trust check passed — executing action: ${action.name}`);
602
+ const handler = this.actionHandlers.get(action.name);
603
+ if (!handler) {
604
+ console.error(`[OACP] No handler registered for action: ${action.name}`);
605
+ return;
606
+ }
607
+ try {
608
+ const result = await handler(payload, msg);
609
+ // If handler returns a result, auto-send task_result
610
+ if (result && typeof result === 'object') {
611
+ const resultMsgId = await this.send(senderId, 'task_result', result, {
612
+ correlationId: msg.correlation_id,
613
+ });
614
+ // Auto-collect payment if this action has a price
615
+ if (action.price && action.price > 0 && this.agentId) {
616
+ try {
617
+ // Call server-side payment collection — server verifies the task was real
618
+ const response = await fetch(`${this.apiBaseUrl}/collect-payment`, {
619
+ method: 'POST',
620
+ headers: {
621
+ 'Content-Type': 'application/json',
622
+ Authorization: `Bearer ${this.registryToken}`,
623
+ },
624
+ body: JSON.stringify({
625
+ provider_agent_id: this.agentId,
626
+ requester_agent_id: senderId,
627
+ amount: action.price,
628
+ message_id: msg.message_id,
629
+ action_name: action.name,
630
+ }),
631
+ });
632
+ if (response.ok) {
633
+ // deno-lint-ignore no-explicit-any
634
+ const data = (await response.json());
635
+ console.log(`[OACP] 💰 Collected ${action.price} tokens from ${senderId.substring(0, 8)}... (tx: ${data.transaction_id})`);
636
+ }
637
+ else {
638
+ const errText = await response.text();
639
+ console.warn(`[OACP] ⚠️ Payment collection failed (${response.status}): ${errText.substring(0, 100)}`);
640
+ }
641
+ }
642
+ catch (payErr) {
643
+ console.warn(`[OACP] ⚠️ Payment collection error: ${payErr instanceof Error ? payErr.message : payErr}`);
644
+ }
645
+ }
646
+ }
647
+ }
648
+ catch (err) {
649
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
650
+ console.error(`[OACP] Action "${action.name}" failed:`, errorMsg);
651
+ await this.send(senderId, 'task_error', {
652
+ error: 'execution_error',
653
+ message: errorMsg,
654
+ action: action.name,
655
+ }, { correlationId: msg.correlation_id });
656
+ }
657
+ }
658
+ /**
659
+ * Dispatch an incoming message to registered handlers.
660
+ * @private
661
+ */
662
+ dispatchMessage(msg) {
663
+ console.log(`[OACP] Received ${msg.message_type} from ${msg.sender_agent_id?.substring(0, 8)}...`);
664
+ // Dispatch to type-specific handlers
665
+ const typeHandlers = this.messageHandlers.get(msg.message_type) || [];
666
+ for (const handler of typeHandlers) {
667
+ try {
668
+ handler(msg);
669
+ }
670
+ catch (err) {
671
+ console.error(`[OACP] Handler error for ${msg.message_type}:`, err);
672
+ }
673
+ }
674
+ // Dispatch to wildcard handlers
675
+ const wildcardHandlers = this.messageHandlers.get('*') || [];
676
+ for (const handler of wildcardHandlers) {
677
+ try {
678
+ handler(msg);
679
+ }
680
+ catch (err) {
681
+ console.error('[OACP] Wildcard handler error:', err);
682
+ }
683
+ }
684
+ }
685
+ // ============================================================
686
+ // Crypto Utilities (exposed for message signing in M002)
687
+ // ============================================================
688
+ /**
689
+ * Sign a message with this agent's secret key.
690
+ * Returns the hex-encoded Ed25519 signature.
691
+ */
692
+ sign(message) {
693
+ return signMessage(message, this.keypair.secretKey);
694
+ }
695
+ /**
696
+ * Verify a signature against a message and public key.
697
+ */
698
+ verify(signature, message, publicKey) {
699
+ return verifySignature(signature, message, publicKey);
700
+ }
701
+ // ============================================================
702
+ // Lifecycle
703
+ // ============================================================
704
+ /**
705
+ * Gracefully shut down the agent.
706
+ * Stops heartbeat and cleans up resources.
707
+ */
708
+ async shutdown() {
709
+ this.stopHeartbeat();
710
+ this.stopRealtimeSubscription();
711
+ this.messageHandlers.clear();
712
+ console.log(`[OACP] Agent "${this.config.name}" shut down`);
713
+ }
714
+ }
715
+ //# sourceMappingURL=agent.js.map