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/LICENSE +202 -0
- package/README.md +111 -0
- package/dist/agent.d.ts +67 -0
- package/dist/agent.js +798 -0
- package/dist/amqpTransport.d.ts +17 -0
- package/dist/amqpTransport.js +164 -0
- package/dist/capabilities.d.ts +16 -0
- package/dist/capabilities.js +81 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +13 -0
- package/dist/crypto.d.ts +20 -0
- package/dist/crypto.js +173 -0
- package/dist/discovery.d.ts +26 -0
- package/dist/discovery.js +267 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +36 -0
- package/dist/httpSecurity.d.ts +15 -0
- package/dist/httpSecurity.js +83 -0
- package/dist/identity.d.ts +45 -0
- package/dist/identity.js +163 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/jsonSupport.d.ts +8 -0
- package/dist/jsonSupport.js +39 -0
- package/dist/keyProvider.d.ts +50 -0
- package/dist/keyProvider.js +209 -0
- package/dist/messages.d.ts +75 -0
- package/dist/messages.js +102 -0
- package/dist/mqttTransport.d.ts +19 -0
- package/dist/mqttTransport.js +215 -0
- package/dist/options.d.ts +34 -0
- package/dist/options.js +109 -0
- package/dist/overlay.d.ts +37 -0
- package/dist/overlay.js +95 -0
- package/dist/overlayFramework.d.ts +44 -0
- package/dist/overlayFramework.js +129 -0
- package/dist/transport.d.ts +16 -0
- package/dist/transport.js +63 -0
- package/dist/wellKnown.d.ts +14 -0
- package/dist/wellKnown.js +202 -0
- package/package.json +51 -0
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
|
+
}
|