acp-runtime 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,798 @@
1
+ /*
2
+ * Copyright 2026 ACP Project
3
+ * Licensed under the Apache License, Version 2.0
4
+ * See LICENSE file for details.
5
+ */
6
+ import { randomUUID } from "node:crypto";
7
+ import { mkdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { AgentCapabilities } from "./capabilities.js";
10
+ import { ACP_VERSION, DEFAULT_CRYPTO_SUITE } from "./constants.js";
11
+ import { signBytes, decryptForRecipient, encryptForRecipients, signProtectedPayload, verifyProtectedPayloadSignature } from "./crypto.js";
12
+ import { DiscoveryClient } from "./discovery.js";
13
+ import { processingError, transportError, validationError, keyProviderError } from "./errors.js";
14
+ import { validateHttpClientPolicy, validateHttpUrl, warnIfInsecureHttpUsed } from "./httpSecurity.js";
15
+ import { buildIdentityDocument, createIdentity, identityFromProvider, parseAgentId, readIdentity, verifyIdentityDocument, writeIdentity } from "./identity.js";
16
+ import { canonicalJsonBytes } from "./jsonSupport.js";
17
+ import { buildAckPayload, buildEnvelope, buildFailPayload, isExpired, messageToMap, parseAcpMessage, parseMessageClass } from "./messages.js";
18
+ import { AmqpTransportClient } from "./amqpTransport.js";
19
+ import { MqttTransportClient } from "./mqttTransport.js";
20
+ import { defaultAgentOptions } from "./options.js";
21
+ import { LocalKeyProvider, VaultKeyProvider } from "./keyProvider.js";
22
+ import { TransportClient } from "./transport.js";
23
+ import { buildWellKnownDocument } from "./wellKnown.js";
24
+ class DedupStore {
25
+ ttlMs;
26
+ processed = new Map();
27
+ constructor(ttlMs) {
28
+ this.ttlMs = ttlMs;
29
+ }
30
+ isDuplicate(messageId) {
31
+ this.cleanup();
32
+ return this.processed.has(messageId);
33
+ }
34
+ markProcessed(messageId) {
35
+ this.processed.set(messageId, Date.now());
36
+ }
37
+ cleanup() {
38
+ const now = Date.now();
39
+ for (const [messageId, timestamp] of this.processed.entries()) {
40
+ if (now - timestamp > this.ttlMs) {
41
+ this.processed.delete(messageId);
42
+ }
43
+ }
44
+ }
45
+ }
46
+ function firstNonBlank(...values) {
47
+ for (const value of values) {
48
+ if (value && value.trim()) {
49
+ return value.trim();
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ function failedOutcome(recipient, reasonCode, detail) {
55
+ return {
56
+ recipient,
57
+ state: "FAILED",
58
+ reason_code: reasonCode,
59
+ detail
60
+ };
61
+ }
62
+ function toPublicKeyMap(targets) {
63
+ const output = {};
64
+ for (const target of targets) {
65
+ output[target.recipient] = target.public_key;
66
+ }
67
+ return output;
68
+ }
69
+ function buildLocalAmqpService(agentId, options) {
70
+ if (!options.amqp_broker_url) {
71
+ return undefined;
72
+ }
73
+ return AmqpTransportClient.buildServiceHint(agentId, options.amqp_broker_url, options.amqp_exchange);
74
+ }
75
+ function buildLocalMqttService(agentId, options) {
76
+ if (!options.mqtt_broker_url) {
77
+ return undefined;
78
+ }
79
+ return MqttTransportClient.buildServiceHint(agentId, options.mqtt_broker_url, undefined, options.mqtt_qos, options.mqtt_topic_prefix);
80
+ }
81
+ function applyHttpSecurityProfile(identityDocument, mtlsEnabled) {
82
+ const serviceRaw = identityDocument.service;
83
+ const service = serviceRaw && typeof serviceRaw === "object" && !Array.isArray(serviceRaw) ? serviceRaw : {};
84
+ const directEndpoint = service.direct_endpoint;
85
+ if (typeof directEndpoint === "string" && directEndpoint.trim()) {
86
+ service.http = {
87
+ endpoint: directEndpoint.trim(),
88
+ security_profile: mtlsEnabled ? "mtls" : directEndpoint.startsWith("https://") ? "https" : "http"
89
+ };
90
+ }
91
+ if (Array.isArray(service.relay_hints) && service.relay_hints.length > 0) {
92
+ const endpoint = typeof service.relay_hints[0] === "string" ? service.relay_hints[0] : undefined;
93
+ if (endpoint) {
94
+ service.relay = {
95
+ endpoint,
96
+ security_profile: mtlsEnabled ? "mtls" : endpoint.startsWith("https://") ? "https" : "http"
97
+ };
98
+ }
99
+ }
100
+ identityDocument.service = service;
101
+ }
102
+ function resignIdentityDocument(identityDocument, identity) {
103
+ const unsigned = { ...identityDocument };
104
+ delete unsigned.signature;
105
+ identityDocument.signature = {
106
+ algorithm: "Ed25519",
107
+ signed_by: identity.signing_kid,
108
+ value: signBytes(canonicalJsonBytes(unsigned), identity.signing_private_key)
109
+ };
110
+ }
111
+ function parseReasonForCapabilityMismatch(reason) {
112
+ const normalized = (reason ?? "").toLowerCase();
113
+ if (normalized.includes("protocol")) {
114
+ return "UNSUPPORTED_VERSION";
115
+ }
116
+ if (normalized.includes("crypto")) {
117
+ return "UNSUPPORTED_CRYPTO_SUITE";
118
+ }
119
+ if (normalized.includes("profile")) {
120
+ return "UNSUPPORTED_PROFILE";
121
+ }
122
+ return "POLICY_REJECTED";
123
+ }
124
+ function deliveryStateFromResponse(statusCode, responseClass, reasonCode) {
125
+ if (statusCode >= 200 && statusCode < 300) {
126
+ if (responseClass === "FAIL") {
127
+ if (reasonCode === "EXPIRED_MESSAGE") {
128
+ return "EXPIRED";
129
+ }
130
+ if (reasonCode === "POLICY_REJECTED") {
131
+ return "DECLINED";
132
+ }
133
+ return "FAILED";
134
+ }
135
+ if (responseClass === "ACK" || responseClass === "CAPABILITIES") {
136
+ return "ACKNOWLEDGED";
137
+ }
138
+ return "DELIVERED";
139
+ }
140
+ if (statusCode === 410) {
141
+ return "EXPIRED";
142
+ }
143
+ if ([401, 403, 409, 422].includes(statusCode)) {
144
+ return "DECLINED";
145
+ }
146
+ return "FAILED";
147
+ }
148
+ function outcomeFromHttpResponse(recipient, response) {
149
+ const body = response.body;
150
+ const responseMessage = body?.response_message && typeof body.response_message === "object" && !Array.isArray(body.response_message)
151
+ ? body.response_message
152
+ : undefined;
153
+ const responseClass = parseMessageClass(responseMessage && responseMessage.envelope && typeof responseMessage.envelope === "object"
154
+ ? responseMessage.envelope.message_class
155
+ : undefined);
156
+ const reasonCode = typeof body?.reason_code === "string" ? body.reason_code : undefined;
157
+ const detail = typeof body?.detail === "string"
158
+ ? body.detail
159
+ : response.status_code >= 400
160
+ ? `Recipient HTTP ${response.status_code}`
161
+ : undefined;
162
+ return {
163
+ recipient,
164
+ state: deliveryStateFromResponse(response.status_code, responseClass, reasonCode),
165
+ status_code: response.status_code,
166
+ response_class: responseClass,
167
+ reason_code: reasonCode,
168
+ detail,
169
+ response_message: responseMessage
170
+ };
171
+ }
172
+ export class AcpAgent {
173
+ identity;
174
+ identity_document;
175
+ discovery;
176
+ transport;
177
+ amqp_transport;
178
+ mqtt_transport;
179
+ capabilities;
180
+ storage_dir;
181
+ trust_profile;
182
+ relay_url;
183
+ default_delivery_mode;
184
+ key_provider_info;
185
+ dedup = new DedupStore(60 * 60 * 1000);
186
+ delivery_states = new Map();
187
+ constructor(identity, identity_document, discovery, transport, amqp_transport, mqtt_transport, capabilities, storage_dir, trust_profile, relay_url, default_delivery_mode, key_provider_info) {
188
+ this.identity = identity;
189
+ this.identity_document = identity_document;
190
+ this.discovery = discovery;
191
+ this.transport = transport;
192
+ this.amqp_transport = amqp_transport;
193
+ this.mqtt_transport = mqtt_transport;
194
+ this.capabilities = capabilities;
195
+ this.storage_dir = storage_dir;
196
+ this.trust_profile = trust_profile;
197
+ this.relay_url = relay_url;
198
+ this.default_delivery_mode = default_delivery_mode;
199
+ this.key_provider_info = key_provider_info;
200
+ }
201
+ static async loadOrCreate(agentId, optionsInput) {
202
+ parseAgentId(agentId);
203
+ const options = { ...defaultAgentOptions(), ...optionsInput };
204
+ mkdirSync(options.storage_dir, { recursive: true });
205
+ const keyProvider = await AcpAgent.resolveKeyProvider(options);
206
+ const keyProviderInfo = keyProvider.describe();
207
+ const providerIdentityKeys = await keyProvider.loadIdentityKeys(agentId).catch(() => undefined);
208
+ const providerTls = await keyProvider.loadTlsMaterial(agentId).catch(() => ({}));
209
+ const providerCa = await keyProvider.loadCaBundle(agentId).catch(() => undefined);
210
+ const externalKeyProvider = keyProviderInfo.provider === "vault";
211
+ const effectiveCaFile = firstNonBlank(options.ca_file, providerTls.ca_file, providerCa);
212
+ const effectiveCertFile = firstNonBlank(options.cert_file, providerTls.cert_file);
213
+ const effectiveKeyFile = firstNonBlank(options.key_file, providerTls.key_file);
214
+ const policy = {
215
+ allow_insecure_http: options.allow_insecure_http,
216
+ allow_insecure_tls: options.allow_insecure_tls,
217
+ mtls_enabled: options.mtls_enabled,
218
+ ca_file: effectiveCaFile,
219
+ cert_file: effectiveCertFile,
220
+ key_file: effectiveKeyFile
221
+ };
222
+ validateHttpClientPolicy(policy, "Agent HTTP security configuration");
223
+ if (options.endpoint) {
224
+ validateHttpUrl(options.endpoint, policy.allow_insecure_http, policy.mtls_enabled, "Agent direct endpoint configuration");
225
+ if (policy.allow_insecure_http) {
226
+ warnIfInsecureHttpUsed(options.endpoint, "Agent direct endpoint configuration");
227
+ }
228
+ }
229
+ validateHttpUrl(options.relay_url, policy.allow_insecure_http, policy.mtls_enabled, "Agent relay URL configuration");
230
+ for (const relayHint of options.relay_hints) {
231
+ validateHttpUrl(relayHint, policy.allow_insecure_http, policy.mtls_enabled, "Agent relay hint configuration");
232
+ }
233
+ for (const directoryHint of options.enterprise_directory_hints) {
234
+ validateHttpUrl(directoryHint, policy.allow_insecure_http, policy.mtls_enabled, "Agent enterprise directory hint configuration");
235
+ }
236
+ const localAmqpService = buildLocalAmqpService(agentId, options);
237
+ const localMqttService = buildLocalMqttService(agentId, options);
238
+ const bundle = readIdentity(options.storage_dir, agentId);
239
+ let identity;
240
+ let identityDocument;
241
+ let capabilities;
242
+ if (!bundle) {
243
+ if (providerIdentityKeys) {
244
+ identity = identityFromProvider({
245
+ agent_id: agentId,
246
+ ...providerIdentityKeys
247
+ });
248
+ }
249
+ else if (externalKeyProvider) {
250
+ throw keyProviderError("Unable to load identity keys from key provider");
251
+ }
252
+ else {
253
+ identity = createIdentity(agentId);
254
+ }
255
+ capabilities = new AgentCapabilities(agentId);
256
+ identityDocument = buildIdentityDocument({
257
+ identity,
258
+ direct_endpoint: options.endpoint,
259
+ relay_hints: options.relay_hints,
260
+ trust_profile: options.trust_profile,
261
+ capabilities: capabilities.toMap(),
262
+ valid_days: 365,
263
+ amqp_service: localAmqpService,
264
+ mqtt_service: localMqttService,
265
+ http_security_profile: options.mtls_enabled ? "mtls" : undefined,
266
+ relay_security_profile: options.mtls_enabled ? "mtls" : undefined
267
+ });
268
+ applyHttpSecurityProfile(identityDocument, options.mtls_enabled);
269
+ resignIdentityDocument(identityDocument, identity);
270
+ writeIdentity(options.storage_dir, identity, identityDocument);
271
+ }
272
+ else {
273
+ identity = bundle.identity;
274
+ identityDocument = bundle.identity_document;
275
+ if (providerIdentityKeys) {
276
+ identity = identityFromProvider({
277
+ agent_id: identity.agent_id,
278
+ ...providerIdentityKeys,
279
+ signing_kid: providerIdentityKeys.signing_kid ?? identity.signing_kid,
280
+ encryption_kid: providerIdentityKeys.encryption_kid ?? identity.encryption_kid
281
+ });
282
+ }
283
+ else if (externalKeyProvider) {
284
+ throw keyProviderError("Unable to load identity keys from key provider");
285
+ }
286
+ capabilities = AgentCapabilities.fromMap(identityDocument.capabilities && typeof identityDocument.capabilities === "object"
287
+ ? identityDocument.capabilities
288
+ : undefined, agentId);
289
+ const shouldRewrite = !verifyIdentityDocument(identityDocument) ||
290
+ Boolean(options.endpoint) ||
291
+ options.relay_hints.length > 0 ||
292
+ Boolean(localAmqpService) ||
293
+ Boolean(localMqttService);
294
+ if (shouldRewrite) {
295
+ const serviceRaw = identityDocument.service;
296
+ const service = serviceRaw && typeof serviceRaw === "object" && !Array.isArray(serviceRaw)
297
+ ? serviceRaw
298
+ : {};
299
+ const existingEndpoint = typeof service.direct_endpoint === "string" ? service.direct_endpoint : undefined;
300
+ const existingRelayHints = Array.isArray(service.relay_hints)
301
+ ? service.relay_hints.filter((item) => typeof item === "string")
302
+ : [];
303
+ identityDocument = buildIdentityDocument({
304
+ identity,
305
+ direct_endpoint: options.endpoint ?? existingEndpoint,
306
+ relay_hints: options.relay_hints.length > 0 ? options.relay_hints : existingRelayHints,
307
+ trust_profile: options.trust_profile,
308
+ capabilities: capabilities.toMap(),
309
+ valid_days: 365,
310
+ amqp_service: localAmqpService ?? service.amqp,
311
+ mqtt_service: localMqttService ?? service.mqtt,
312
+ http_security_profile: options.mtls_enabled ? "mtls" : undefined,
313
+ relay_security_profile: options.mtls_enabled ? "mtls" : undefined
314
+ });
315
+ applyHttpSecurityProfile(identityDocument, options.mtls_enabled);
316
+ resignIdentityDocument(identityDocument, identity);
317
+ writeIdentity(options.storage_dir, identity, identityDocument);
318
+ }
319
+ }
320
+ const effectiveRelayHints = options.relay_hints.length > 0
321
+ ? options.relay_hints
322
+ : Array.isArray(identityDocument.service?.relay_hints)
323
+ ? (identityDocument.service.relay_hints ?? []).filter((item) => typeof item === "string")
324
+ : [];
325
+ const discovery = new DiscoveryClient(join(options.storage_dir, "discovery-cache.json"), options.discovery_scheme, effectiveRelayHints, options.enterprise_directory_hints, options.http_timeout_seconds, policy);
326
+ discovery.seed(identityDocument);
327
+ const transport = new TransportClient(options.http_timeout_seconds, policy);
328
+ const amqpTransport = options.amqp_broker_url
329
+ ? new AmqpTransportClient(options.amqp_broker_url, options.amqp_exchange, options.amqp_exchange_type, options.http_timeout_seconds)
330
+ : undefined;
331
+ const mqttTransport = options.mqtt_broker_url
332
+ ? new MqttTransportClient(options.mqtt_broker_url, options.mqtt_qos, options.mqtt_topic_prefix, options.http_timeout_seconds)
333
+ : undefined;
334
+ return new AcpAgent(identity, identityDocument, discovery, transport, amqpTransport, mqttTransport, capabilities, options.storage_dir, options.trust_profile, options.relay_url, options.default_delivery_mode, keyProviderInfo);
335
+ }
336
+ static async resolveKeyProvider(options) {
337
+ if (options.key_provider === "vault") {
338
+ if (!options.vault_url || !options.vault_path) {
339
+ throw keyProviderError("vault_url and vault_path are required when key_provider=vault");
340
+ }
341
+ return new VaultKeyProvider(options.vault_url, options.vault_path, options.vault_token_env, options.vault_token, options.http_timeout_seconds, options.ca_file, options.allow_insecure_tls, options.allow_insecure_http);
342
+ }
343
+ return new LocalKeyProvider(options.storage_dir, options.cert_file, options.key_file, options.ca_file);
344
+ }
345
+ agentId() {
346
+ return this.identity.agent_id;
347
+ }
348
+ getDeliveryStates() {
349
+ const output = {};
350
+ for (const [operationId, states] of this.delivery_states.entries()) {
351
+ output[operationId] = Object.fromEntries(states.entries());
352
+ }
353
+ return output;
354
+ }
355
+ buildWellKnownDocument(baseUrl, identityDocumentUrl) {
356
+ const resolvedBaseUrl = baseUrl ??
357
+ (() => {
358
+ const service = this.identity_document.service;
359
+ const endpoint = service?.direct_endpoint;
360
+ if (typeof endpoint !== "string" || !endpoint.trim()) {
361
+ return undefined;
362
+ }
363
+ const parsed = new URL(endpoint);
364
+ return `${parsed.protocol}//${parsed.host}`;
365
+ })();
366
+ if (!resolvedBaseUrl) {
367
+ throw validationError("Unable to build /.well-known/acp metadata without base_url or direct_endpoint");
368
+ }
369
+ return buildWellKnownDocument({
370
+ identity_document: this.identity_document,
371
+ base_url: resolvedBaseUrl,
372
+ identity_document_url: identityDocumentUrl,
373
+ version: ACP_VERSION
374
+ });
375
+ }
376
+ registerIdentityDocument(identityDocument) {
377
+ this.discovery.registerIdentityDocument(identityDocument);
378
+ }
379
+ async resolveWellKnown(baseUrl, expectedAgentId) {
380
+ return this.discovery.resolveWellKnown(baseUrl, expectedAgentId);
381
+ }
382
+ async resolveRecipients(recipients, mode) {
383
+ const deliverable = [];
384
+ const preflight = [];
385
+ for (const recipient of recipients) {
386
+ let identityDocument;
387
+ try {
388
+ identityDocument = await this.discovery.resolve(recipient);
389
+ }
390
+ catch (error) {
391
+ preflight.push(failedOutcome(recipient, "POLICY_REJECTED", String(error)));
392
+ continue;
393
+ }
394
+ const remoteCapabilities = AgentCapabilities.fromMap(identityDocument.capabilities && typeof identityDocument.capabilities === "object"
395
+ ? identityDocument.capabilities
396
+ : undefined, recipient);
397
+ const capabilityMatch = this.capabilities.chooseCompatible(remoteCapabilities);
398
+ if (!capabilityMatch.compatible) {
399
+ preflight.push(failedOutcome(recipient, parseReasonForCapabilityMismatch(capabilityMatch.reason), capabilityMatch.reason ?? "No compatible capabilities"));
400
+ continue;
401
+ }
402
+ const choice = this.chooseDeliveryChannel(remoteCapabilities, identityDocument, mode);
403
+ if (!choice.channel) {
404
+ preflight.push(failedOutcome(recipient, "POLICY_REJECTED", choice.detail ?? "Delivery channel unavailable"));
405
+ continue;
406
+ }
407
+ const publicKey = identityDocument.keys?.encryption
408
+ ?.public_key ?? "";
409
+ if (!publicKey.trim()) {
410
+ preflight.push(failedOutcome(recipient, "POLICY_REJECTED", "Recipient identity document missing encryption public key"));
411
+ continue;
412
+ }
413
+ deliverable.push({
414
+ recipient,
415
+ public_key: publicKey.trim(),
416
+ channel: choice.channel,
417
+ endpoint: choice.endpoint,
418
+ amqp_service: choice.amqp_service,
419
+ mqtt_service: choice.mqtt_service
420
+ });
421
+ }
422
+ return { deliverable, preflight_outcomes: preflight };
423
+ }
424
+ chooseDeliveryChannel(remoteCapabilities, identityDocument, mode) {
425
+ const shared = this.capabilities.transports
426
+ .map((item) => item.toLowerCase())
427
+ .filter((transport) => remoteCapabilities.transports.map((t) => t.toLowerCase()).includes(transport));
428
+ const serviceRaw = identityDocument.service;
429
+ const service = serviceRaw && typeof serviceRaw === "object" && !Array.isArray(serviceRaw)
430
+ ? serviceRaw
431
+ : {};
432
+ const directEndpoint = typeof service.direct_endpoint === "string" ? service.direct_endpoint.trim() : "";
433
+ const hasDirect = Boolean(directEndpoint);
434
+ const amqpService = service.amqp && typeof service.amqp === "object" && !Array.isArray(service.amqp)
435
+ ? service.amqp
436
+ : undefined;
437
+ const mqttService = service.mqtt && typeof service.mqtt === "object" && !Array.isArray(service.mqtt)
438
+ ? service.mqtt
439
+ : undefined;
440
+ const directAvailable = hasDirect && shared.some((t) => t === "https" || t === "http" || t === "direct");
441
+ const relayAvailable = this.relay_url.trim().length > 0 && shared.includes("relay");
442
+ const amqpAvailable = Boolean(amqpService) && shared.includes("amqp");
443
+ const mqttAvailable = Boolean(mqttService) && shared.includes("mqtt");
444
+ if (mode === "direct") {
445
+ if (directAvailable) {
446
+ return { channel: "direct", endpoint: directEndpoint };
447
+ }
448
+ return { detail: "Recipient direct endpoint is unavailable or incompatible" };
449
+ }
450
+ if (mode === "relay") {
451
+ if (relayAvailable) {
452
+ return { channel: "relay" };
453
+ }
454
+ return { detail: "Relay delivery is unavailable or incompatible" };
455
+ }
456
+ if (mode === "amqp") {
457
+ if (amqpAvailable) {
458
+ return { channel: "amqp", amqp_service: amqpService };
459
+ }
460
+ return { detail: "AMQP delivery is unavailable or incompatible" };
461
+ }
462
+ if (mode === "mqtt") {
463
+ if (mqttAvailable) {
464
+ return { channel: "mqtt", mqtt_service: mqttService };
465
+ }
466
+ return { detail: "MQTT delivery is unavailable or incompatible" };
467
+ }
468
+ if (directAvailable) {
469
+ return { channel: "direct", endpoint: directEndpoint };
470
+ }
471
+ if (relayAvailable) {
472
+ return { channel: "relay" };
473
+ }
474
+ if (amqpAvailable) {
475
+ return { channel: "amqp", amqp_service: amqpService };
476
+ }
477
+ if (mqttAvailable) {
478
+ return { channel: "mqtt", mqtt_service: mqttService };
479
+ }
480
+ return {
481
+ detail: "Recipient identity document is missing direct_endpoint/amqp/mqtt and no relay fallback is compatible"
482
+ };
483
+ }
484
+ buildMessage(recipients, payload, recipientPublicKeys, messageClass, contextId, operationId, expiresInSeconds, correlationId, inReplyTo) {
485
+ const envelope = buildEnvelope({
486
+ sender: this.agentId(),
487
+ recipients,
488
+ message_class: messageClass,
489
+ context_id: contextId,
490
+ operation_id: operationId,
491
+ expires_in_seconds: expiresInSeconds,
492
+ correlation_id: correlationId,
493
+ in_reply_to: inReplyTo,
494
+ crypto_suite: DEFAULT_CRYPTO_SUITE
495
+ });
496
+ let protectedPayload = encryptForRecipients(payload, envelope, recipientPublicKeys);
497
+ protectedPayload = signProtectedPayload(envelope, protectedPayload, this.identity.signing_private_key, this.identity.signing_kid);
498
+ return {
499
+ envelope,
500
+ protected: protectedPayload,
501
+ sender_identity_document: this.identity_document
502
+ };
503
+ }
504
+ async deliverDirect(message, targets) {
505
+ const messageMap = messageToMap(message);
506
+ const outcomes = [];
507
+ for (const target of targets) {
508
+ if (!target.endpoint) {
509
+ outcomes.push(failedOutcome(target.recipient, "POLICY_REJECTED", "Recipient direct endpoint missing"));
510
+ continue;
511
+ }
512
+ try {
513
+ const response = await this.transport.postJson(target.endpoint, messageMap);
514
+ outcomes.push(outcomeFromHttpResponse(target.recipient, response));
515
+ }
516
+ catch (error) {
517
+ outcomes.push(failedOutcome(target.recipient, "POLICY_REJECTED", `Direct transport failure: ${String(error)}`));
518
+ }
519
+ }
520
+ return outcomes;
521
+ }
522
+ async deliverRelay(message, targets) {
523
+ const outcomes = [];
524
+ try {
525
+ const relayResponse = await this.transport.sendToRelay(this.relay_url, message);
526
+ const relayOutcomes = Array.isArray(relayResponse.outcomes) && relayResponse.outcomes.length > 0
527
+ ? relayResponse.outcomes
528
+ : targets.map((target) => ({ recipient: target.recipient, state: "DELIVERED" }));
529
+ for (const relayOutcome of relayOutcomes) {
530
+ const item = relayOutcome && typeof relayOutcome === "object" && !Array.isArray(relayOutcome)
531
+ ? relayOutcome
532
+ : {};
533
+ outcomes.push({
534
+ recipient: typeof item.recipient === "string" ? item.recipient : "",
535
+ state: (typeof item.state === "string" ? item.state : "DELIVERED"),
536
+ status_code: typeof item.status_code === "number" ? item.status_code : undefined,
537
+ response_class: parseMessageClass(item.response_class),
538
+ reason_code: typeof item.reason_code === "string" ? item.reason_code : undefined,
539
+ detail: typeof item.detail === "string" ? item.detail : undefined,
540
+ response_message: item.response_message && typeof item.response_message === "object" && !Array.isArray(item.response_message)
541
+ ? item.response_message
542
+ : undefined
543
+ });
544
+ }
545
+ }
546
+ catch (error) {
547
+ for (const target of targets) {
548
+ outcomes.push(failedOutcome(target.recipient, "POLICY_REJECTED", `Relay transport failure: ${String(error)}`));
549
+ }
550
+ }
551
+ return outcomes;
552
+ }
553
+ async deliverAmqp(message, target) {
554
+ const outcome = { recipient: target.recipient, state: "PENDING" };
555
+ try {
556
+ const client = this.amqp_transport;
557
+ if (!client && !target.amqp_service) {
558
+ throw transportError("AMQP delivery selected but sender is not configured with an AMQP broker");
559
+ }
560
+ const brokerUrl = target.amqp_service?.broker_url ?? this.amqp_transport?.broker_url;
561
+ if (!client && !brokerUrl) {
562
+ throw transportError("AMQP delivery selected but sender is not configured with an AMQP broker");
563
+ }
564
+ const transportClient = client ??
565
+ new AmqpTransportClient(brokerUrl, target.amqp_service?.exchange, undefined, 10);
566
+ await transportClient.publish(messageToMap(message), target.recipient, target.amqp_service);
567
+ outcome.state = "DELIVERED";
568
+ }
569
+ catch (error) {
570
+ outcome.state = "FAILED";
571
+ outcome.reason_code = "POLICY_REJECTED";
572
+ outcome.detail = `AMQP transport failure: ${String(error)}`;
573
+ }
574
+ return outcome;
575
+ }
576
+ async deliverMqtt(message, target) {
577
+ const outcome = { recipient: target.recipient, state: "PENDING" };
578
+ try {
579
+ const client = this.mqtt_transport;
580
+ if (!client && !target.mqtt_service) {
581
+ throw transportError("MQTT delivery selected but sender is not configured with an MQTT broker");
582
+ }
583
+ const brokerUrl = target.mqtt_service?.broker_url ?? this.mqtt_transport?.broker_url;
584
+ if (!client && !brokerUrl) {
585
+ throw transportError("MQTT delivery selected but sender is not configured with an MQTT broker");
586
+ }
587
+ const transportClient = client ??
588
+ new MqttTransportClient(brokerUrl, Number(target.mqtt_service?.qos ?? 1), this.default_delivery_mode === "mqtt" ? undefined : "acp/agent", 10);
589
+ await transportClient.publish(messageToMap(message), target.recipient, target.mqtt_service);
590
+ outcome.state = "DELIVERED";
591
+ }
592
+ catch (error) {
593
+ outcome.state = "FAILED";
594
+ outcome.reason_code = "POLICY_REJECTED";
595
+ outcome.detail = `MQTT transport failure: ${String(error)}`;
596
+ }
597
+ return outcome;
598
+ }
599
+ syncDeliveryStates(operationId, outcomes) {
600
+ const states = new Map();
601
+ for (const outcome of outcomes) {
602
+ states.set(outcome.recipient, outcome.state);
603
+ }
604
+ this.delivery_states.set(operationId, states);
605
+ }
606
+ async send(recipients, payload, context, messageClass = "SEND", expiresInSeconds = 300, correlationId, inReplyTo, deliveryMode) {
607
+ if (recipients.length === 0) {
608
+ throw validationError("send() requires at least one recipient");
609
+ }
610
+ const mode = deliveryMode ?? this.default_delivery_mode;
611
+ const operationId = randomUUID();
612
+ const contextId = context ?? `ctx:${randomUUID()}`;
613
+ const resolved = await this.resolveRecipients(recipients, mode);
614
+ const outcomes = [...resolved.preflight_outcomes];
615
+ const messageIds = [];
616
+ const directTargets = resolved.deliverable.filter((target) => target.channel === "direct");
617
+ const relayTargets = resolved.deliverable.filter((target) => target.channel === "relay");
618
+ const amqpTargets = resolved.deliverable.filter((target) => target.channel === "amqp");
619
+ const mqttTargets = resolved.deliverable.filter((target) => target.channel === "mqtt");
620
+ if (directTargets.length > 0) {
621
+ const message = this.buildMessage(directTargets.map((target) => target.recipient), payload, toPublicKeyMap(directTargets), messageClass, contextId, operationId, expiresInSeconds, correlationId, inReplyTo);
622
+ messageIds.push(message.envelope.message_id);
623
+ outcomes.push(...(await this.deliverDirect(message, directTargets)));
624
+ }
625
+ if (relayTargets.length > 0) {
626
+ const message = this.buildMessage(relayTargets.map((target) => target.recipient), payload, toPublicKeyMap(relayTargets), messageClass, contextId, operationId, expiresInSeconds, correlationId, inReplyTo);
627
+ messageIds.push(message.envelope.message_id);
628
+ outcomes.push(...(await this.deliverRelay(message, relayTargets)));
629
+ }
630
+ for (const target of amqpTargets) {
631
+ const message = this.buildMessage([target.recipient], payload, { [target.recipient]: target.public_key }, messageClass, contextId, operationId, expiresInSeconds, correlationId, inReplyTo);
632
+ messageIds.push(message.envelope.message_id);
633
+ outcomes.push(await this.deliverAmqp(message, target));
634
+ }
635
+ for (const target of mqttTargets) {
636
+ const message = this.buildMessage([target.recipient], payload, { [target.recipient]: target.public_key }, messageClass, contextId, operationId, expiresInSeconds, correlationId, inReplyTo);
637
+ messageIds.push(message.envelope.message_id);
638
+ outcomes.push(await this.deliverMqtt(message, target));
639
+ }
640
+ if (messageIds.length === 0) {
641
+ messageIds.push(randomUUID());
642
+ }
643
+ const result = {
644
+ operation_id: operationId,
645
+ message_id: messageIds[0],
646
+ message_ids: messageIds,
647
+ outcomes
648
+ };
649
+ this.syncDeliveryStates(operationId, outcomes);
650
+ return result;
651
+ }
652
+ async sendBasic(recipients, payload, context) {
653
+ return this.send(recipients, payload, context, "SEND", 300, undefined, undefined, this.default_delivery_mode);
654
+ }
655
+ async resolveSenderIdentityDocument(rawMessage, senderId) {
656
+ if (rawMessage.sender_identity_document &&
657
+ typeof rawMessage.sender_identity_document === "object" &&
658
+ !Array.isArray(rawMessage.sender_identity_document)) {
659
+ const embedded = rawMessage.sender_identity_document;
660
+ if (embedded.agent_id === senderId && verifyIdentityDocument(embedded)) {
661
+ return embedded;
662
+ }
663
+ }
664
+ return this.discovery.resolve(senderId);
665
+ }
666
+ validateEnvelopeForInbound(envelope) {
667
+ if (envelope.acp_version !== ACP_VERSION) {
668
+ throw processingError("UNSUPPORTED_VERSION", `Unsupported ACP version: ${envelope.acp_version}`);
669
+ }
670
+ if (envelope.crypto_suite !== DEFAULT_CRYPTO_SUITE) {
671
+ throw processingError("UNSUPPORTED_CRYPTO_SUITE", `Unsupported crypto suite: ${envelope.crypto_suite}`);
672
+ }
673
+ if (isExpired(envelope)) {
674
+ throw processingError("EXPIRED_MESSAGE", "Message is expired");
675
+ }
676
+ }
677
+ createResponseMessage(senderIdentityDocument, requestEnvelope, responseClass, responsePayload) {
678
+ const senderId = requestEnvelope.sender;
679
+ const senderPublicKey = (senderIdentityDocument.keys.encryption.public_key ?? "").trim();
680
+ if (!senderPublicKey) {
681
+ throw processingError("POLICY_REJECTED", "Sender identity document missing encryption key");
682
+ }
683
+ return this.buildMessage([senderId], responsePayload, { [senderId]: senderPublicKey }, responseClass, requestEnvelope.context_id, requestEnvelope.operation_id, 300, requestEnvelope.correlation_id ?? requestEnvelope.operation_id, requestEnvelope.message_id);
684
+ }
685
+ async decryptMessageForSelf(rawMessage) {
686
+ const message = parseAcpMessage(rawMessage);
687
+ this.validateEnvelopeForInbound(message.envelope);
688
+ if (!message.envelope.recipients.includes(this.agentId())) {
689
+ throw processingError("POLICY_REJECTED", "Message is not addressed to this agent");
690
+ }
691
+ const senderDoc = await this.resolveSenderIdentityDocument(rawMessage, message.envelope.sender);
692
+ const senderSigningKey = (senderDoc.keys.signing.public_key ?? "").trim();
693
+ if (!senderSigningKey) {
694
+ throw processingError("INVALID_SIGNATURE", "Sender signing public key missing");
695
+ }
696
+ if (!verifyProtectedPayloadSignature(message.envelope, message.protected, senderSigningKey)) {
697
+ throw processingError("INVALID_SIGNATURE", "Message signature verification failed");
698
+ }
699
+ const payload = decryptForRecipient(message.envelope, message.protected, this.agentId(), this.identity.encryption_private_key);
700
+ return { message, payload };
701
+ }
702
+ async receive(rawMessage, handler) {
703
+ const result = {
704
+ state: "FAILED"
705
+ };
706
+ let requestMessage;
707
+ try {
708
+ requestMessage = parseAcpMessage(rawMessage);
709
+ }
710
+ catch (error) {
711
+ result.reason_code = "POLICY_REJECTED";
712
+ result.detail = `Invalid ACP message structure: ${String(error)}`;
713
+ return result;
714
+ }
715
+ let senderDoc;
716
+ try {
717
+ this.validateEnvelopeForInbound(requestMessage.envelope);
718
+ if (!requestMessage.envelope.recipients.includes(this.agentId())) {
719
+ throw processingError("POLICY_REJECTED", `Recipient ${this.agentId()} not in message recipients`);
720
+ }
721
+ senderDoc = await this.resolveSenderIdentityDocument(rawMessage, requestMessage.envelope.sender);
722
+ const senderSigningKey = (senderDoc.keys.signing.public_key ?? "").trim();
723
+ if (!senderSigningKey) {
724
+ throw processingError("INVALID_SIGNATURE", "Sender signing key missing from identity document");
725
+ }
726
+ if (!verifyProtectedPayloadSignature(requestMessage.envelope, requestMessage.protected, senderSigningKey)) {
727
+ throw processingError("INVALID_SIGNATURE", "Signature verification failed");
728
+ }
729
+ if (this.dedup.isDuplicate(requestMessage.envelope.message_id)) {
730
+ result.state = "ACKNOWLEDGED";
731
+ result.detail = "Duplicate message acknowledged";
732
+ if (requestMessage.envelope.message_class !== "ACK" && requestMessage.envelope.message_class !== "FAIL") {
733
+ result.response_message = messageToMap(this.createResponseMessage(senderDoc, requestMessage.envelope, "ACK", buildAckPayload(requestMessage.envelope.message_id, "duplicate")));
734
+ }
735
+ return result;
736
+ }
737
+ const decrypted = decryptForRecipient(requestMessage.envelope, requestMessage.protected, this.agentId(), this.identity.encryption_private_key);
738
+ result.decrypted_payload = decrypted;
739
+ let responseMessage;
740
+ if (requestMessage.envelope.message_class === "CAPABILITIES") {
741
+ responseMessage = this.createResponseMessage(senderDoc, requestMessage.envelope, "CAPABILITIES", this.capabilities.toMap());
742
+ }
743
+ else if (requestMessage.envelope.message_class !== "ACK" && requestMessage.envelope.message_class !== "FAIL") {
744
+ const ackPayload = buildAckPayload(requestMessage.envelope.message_id, "accepted");
745
+ const handlerPayload = handler?.(decrypted, requestMessage.envelope);
746
+ if (handlerPayload && Object.keys(handlerPayload).length > 0) {
747
+ ackPayload.handler = handlerPayload;
748
+ }
749
+ responseMessage = this.createResponseMessage(senderDoc, requestMessage.envelope, "ACK", ackPayload);
750
+ }
751
+ this.dedup.markProcessed(requestMessage.envelope.message_id);
752
+ result.state = "ACKNOWLEDGED";
753
+ result.response_message = responseMessage ? messageToMap(responseMessage) : undefined;
754
+ return result;
755
+ }
756
+ catch (error) {
757
+ const processing = error instanceof Error ? error : new Error(String(error));
758
+ if (error instanceof Error && "reason" in error && typeof error.reason === "string") {
759
+ result.reason_code = error.reason;
760
+ }
761
+ else {
762
+ result.reason_code = "POLICY_REJECTED";
763
+ }
764
+ result.detail = processing.message;
765
+ const terminal = requestMessage.envelope.message_class === "ACK" || requestMessage.envelope.message_class === "FAIL";
766
+ if (!terminal) {
767
+ try {
768
+ const sender = await this.resolveSenderIdentityDocument(rawMessage, requestMessage.envelope.sender);
769
+ result.response_message = messageToMap(this.createResponseMessage(sender, requestMessage.envelope, "FAIL", buildFailPayload(result.reason_code, result.detail ?? "processing error", false)));
770
+ }
771
+ catch {
772
+ result.response_message = undefined;
773
+ }
774
+ }
775
+ return result;
776
+ }
777
+ }
778
+ async requestCapabilities(recipient) {
779
+ const result = await this.send([recipient], {}, `capabilities:${randomUUID()}`, "CAPABILITIES", 300, undefined, undefined, this.default_delivery_mode);
780
+ let capabilities;
781
+ for (const outcome of result.outcomes) {
782
+ if (!outcome.response_message) {
783
+ continue;
784
+ }
785
+ try {
786
+ const decrypted = await this.decryptMessageForSelf(outcome.response_message);
787
+ if (decrypted.message.envelope.message_class === "CAPABILITIES") {
788
+ capabilities = decrypted.payload;
789
+ break;
790
+ }
791
+ }
792
+ catch {
793
+ continue;
794
+ }
795
+ }
796
+ return { result, capabilities };
797
+ }
798
+ }