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
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 ACP Project
|
|
3
|
+
* Licensed under the Apache License, Version 2.0
|
|
4
|
+
* See LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import mqtt from "mqtt";
|
|
7
|
+
import { invalidArgument, transportError, validationError } from "./errors.js";
|
|
8
|
+
export const DEFAULT_MQTT_QOS = 1;
|
|
9
|
+
export const DEFAULT_MQTT_TOPIC_PREFIX = "acp/agent";
|
|
10
|
+
function toQos(qos) {
|
|
11
|
+
if (qos <= 0) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
if (qos >= 2) {
|
|
15
|
+
return 2;
|
|
16
|
+
}
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
function clampQos(qos) {
|
|
20
|
+
return Math.min(2, Math.max(0, qos));
|
|
21
|
+
}
|
|
22
|
+
function pickString(service, key, fallback) {
|
|
23
|
+
const value = service?.[key];
|
|
24
|
+
if (typeof value === "string" && value.trim()) {
|
|
25
|
+
return value.trim();
|
|
26
|
+
}
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
function valueAsNumber(value) {
|
|
30
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string" && value.trim()) {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
if (Number.isFinite(parsed)) {
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
export function metadataProperties(message) {
|
|
42
|
+
const envelope = message.envelope && typeof message.envelope === "object" && !Array.isArray(message.envelope)
|
|
43
|
+
? message.envelope
|
|
44
|
+
: {};
|
|
45
|
+
const metadata = {};
|
|
46
|
+
for (const [source, destination] of [
|
|
47
|
+
["acp_version", "acp_version"],
|
|
48
|
+
["message_class", "acp_message_class"],
|
|
49
|
+
["message_id", "acp_message_id"],
|
|
50
|
+
["operation_id", "acp_operation_id"],
|
|
51
|
+
["sender", "acp_sender"]
|
|
52
|
+
]) {
|
|
53
|
+
const value = envelope[source];
|
|
54
|
+
if (typeof value === "string" && value.trim()) {
|
|
55
|
+
metadata[destination] = value.trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return metadata;
|
|
59
|
+
}
|
|
60
|
+
export class MqttTransportClient {
|
|
61
|
+
broker_url;
|
|
62
|
+
qos;
|
|
63
|
+
topic_prefix;
|
|
64
|
+
timeout_seconds;
|
|
65
|
+
keepalive_seconds;
|
|
66
|
+
constructor(brokerUrl, qos = DEFAULT_MQTT_QOS, topicPrefix = DEFAULT_MQTT_TOPIC_PREFIX, timeoutSeconds = 10, keepaliveSeconds = 30) {
|
|
67
|
+
if (!brokerUrl.trim()) {
|
|
68
|
+
throw invalidArgument("broker_url must be provided");
|
|
69
|
+
}
|
|
70
|
+
this.broker_url = brokerUrl.trim();
|
|
71
|
+
this.qos = clampQos(qos);
|
|
72
|
+
this.topic_prefix = topicPrefix.trim().replace(/\/+$/, "") || DEFAULT_MQTT_TOPIC_PREFIX;
|
|
73
|
+
this.timeout_seconds = Math.max(1, timeoutSeconds);
|
|
74
|
+
this.keepalive_seconds = Math.max(5, keepaliveSeconds);
|
|
75
|
+
}
|
|
76
|
+
static agentIdentifierToken(agentId) {
|
|
77
|
+
const match = /^agent:(?<name>[^@]+)(?:@(?<domain>.+))?$/.exec(agentId);
|
|
78
|
+
if (!match?.groups?.name) {
|
|
79
|
+
throw validationError(`Invalid agent identifier: ${agentId}`);
|
|
80
|
+
}
|
|
81
|
+
const base = match.groups.domain ? `${match.groups.name}.${match.groups.domain}` : match.groups.name;
|
|
82
|
+
const normalized = base
|
|
83
|
+
.split("")
|
|
84
|
+
.map((char) => (/^[A-Za-z0-9._-]$/.test(char) ? char : "."))
|
|
85
|
+
.join("")
|
|
86
|
+
.split(".")
|
|
87
|
+
.filter((segment) => segment.length > 0)
|
|
88
|
+
.join(".")
|
|
89
|
+
.toLowerCase();
|
|
90
|
+
return normalized || "unknown";
|
|
91
|
+
}
|
|
92
|
+
static topicForAgent(agentId, topicPrefix = DEFAULT_MQTT_TOPIC_PREFIX) {
|
|
93
|
+
return `${topicPrefix.replace(/\/+$/, "")}/${MqttTransportClient.agentIdentifierToken(agentId)}`;
|
|
94
|
+
}
|
|
95
|
+
static buildServiceHint(agentId, brokerUrl, topic, qos = DEFAULT_MQTT_QOS, topicPrefix = DEFAULT_MQTT_TOPIC_PREFIX) {
|
|
96
|
+
return {
|
|
97
|
+
broker_url: brokerUrl,
|
|
98
|
+
topic: topic?.trim() || MqttTransportClient.topicForAgent(agentId, topicPrefix),
|
|
99
|
+
qos: clampQos(qos)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
connectClient(brokerUrl) {
|
|
103
|
+
const options = {
|
|
104
|
+
protocolVersion: 5,
|
|
105
|
+
keepalive: this.keepalive_seconds,
|
|
106
|
+
reconnectPeriod: 0,
|
|
107
|
+
connectTimeout: this.timeout_seconds * 1000
|
|
108
|
+
};
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const client = mqtt.connect(brokerUrl, options);
|
|
111
|
+
const onConnect = () => {
|
|
112
|
+
cleanup();
|
|
113
|
+
resolve(client);
|
|
114
|
+
};
|
|
115
|
+
const onError = (error) => {
|
|
116
|
+
cleanup();
|
|
117
|
+
client.end(true);
|
|
118
|
+
reject(error);
|
|
119
|
+
};
|
|
120
|
+
const cleanup = () => {
|
|
121
|
+
client.off("connect", onConnect);
|
|
122
|
+
client.off("error", onError);
|
|
123
|
+
};
|
|
124
|
+
client.once("connect", onConnect);
|
|
125
|
+
client.once("error", onError);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async publish(message, recipientAgentId, service) {
|
|
129
|
+
const brokerUrl = pickString(service, "broker_url", this.broker_url);
|
|
130
|
+
const topic = pickString(service, "topic", MqttTransportClient.topicForAgent(recipientAgentId, this.topic_prefix));
|
|
131
|
+
const qos = clampQos(valueAsNumber(service?.qos) ?? this.qos);
|
|
132
|
+
const properties = metadataProperties(message);
|
|
133
|
+
const client = await this.connectClient(brokerUrl);
|
|
134
|
+
try {
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
client.publish(topic, JSON.stringify(message), {
|
|
137
|
+
qos: toQos(qos),
|
|
138
|
+
properties: {
|
|
139
|
+
userProperties: properties
|
|
140
|
+
}
|
|
141
|
+
}, (error) => {
|
|
142
|
+
if (error) {
|
|
143
|
+
reject(error);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
throw transportError(`mqtt publish failed: ${String(error)}`);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
client.end(true);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async consume(agentId, handler, service, maxMessages = 0, pollTimeoutMs = 1000) {
|
|
158
|
+
const brokerUrl = pickString(service, "broker_url", this.broker_url);
|
|
159
|
+
const topic = pickString(service, "topic", MqttTransportClient.topicForAgent(agentId, this.topic_prefix));
|
|
160
|
+
const qos = clampQos(valueAsNumber(service?.qos) ?? this.qos);
|
|
161
|
+
const limit = maxMessages === 0 ? Number.MAX_SAFE_INTEGER : maxMessages;
|
|
162
|
+
const client = await this.connectClient(brokerUrl);
|
|
163
|
+
let processed = 0;
|
|
164
|
+
try {
|
|
165
|
+
await new Promise((resolve, reject) => {
|
|
166
|
+
client.subscribe(topic, { qos: toQos(qos) }, (error) => {
|
|
167
|
+
if (error instanceof Error) {
|
|
168
|
+
reject(error);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
resolve();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
await new Promise((resolve) => {
|
|
175
|
+
const timeout = setTimeout(() => {
|
|
176
|
+
cleanup();
|
|
177
|
+
resolve();
|
|
178
|
+
}, pollTimeoutMs);
|
|
179
|
+
const cleanup = () => {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
client.off("message", onMessage);
|
|
182
|
+
};
|
|
183
|
+
const onMessage = async (_topic, payload) => {
|
|
184
|
+
if (processed >= limit) {
|
|
185
|
+
cleanup();
|
|
186
|
+
resolve();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(payload.toString("utf-8"));
|
|
191
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
192
|
+
await handler(parsed);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Keep behavior tolerant: invalid messages are ignored.
|
|
197
|
+
}
|
|
198
|
+
processed += 1;
|
|
199
|
+
if (processed >= limit) {
|
|
200
|
+
cleanup();
|
|
201
|
+
resolve();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
client.on("message", onMessage);
|
|
205
|
+
});
|
|
206
|
+
return processed;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
throw transportError(`mqtt consume failed: ${String(error)}`);
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
client.end(true);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { JsonMap } from "./jsonSupport.js";
|
|
2
|
+
import { DeliveryMode } from "./messages.js";
|
|
3
|
+
export interface AcpAgentOptions {
|
|
4
|
+
storage_dir: string;
|
|
5
|
+
endpoint?: string;
|
|
6
|
+
relay_url: string;
|
|
7
|
+
relay_hints: string[];
|
|
8
|
+
enterprise_directory_hints: string[];
|
|
9
|
+
discovery_scheme: string;
|
|
10
|
+
trust_profile: string;
|
|
11
|
+
default_delivery_mode: DeliveryMode;
|
|
12
|
+
http_timeout_seconds: number;
|
|
13
|
+
allow_insecure_http: boolean;
|
|
14
|
+
allow_insecure_tls: boolean;
|
|
15
|
+
mtls_enabled: boolean;
|
|
16
|
+
ca_file?: string;
|
|
17
|
+
cert_file?: string;
|
|
18
|
+
key_file?: string;
|
|
19
|
+
key_provider: "local" | "vault";
|
|
20
|
+
vault_url?: string;
|
|
21
|
+
vault_path?: string;
|
|
22
|
+
vault_token_env: string;
|
|
23
|
+
vault_token?: string;
|
|
24
|
+
amqp_broker_url?: string;
|
|
25
|
+
amqp_exchange: string;
|
|
26
|
+
amqp_exchange_type: string;
|
|
27
|
+
mqtt_broker_url?: string;
|
|
28
|
+
mqtt_qos: number;
|
|
29
|
+
mqtt_topic_prefix: string;
|
|
30
|
+
extra: JsonMap;
|
|
31
|
+
}
|
|
32
|
+
export declare function defaultAgentOptions(): AcpAgentOptions;
|
|
33
|
+
export declare function optionsFromConfigMap(config: JsonMap | undefined): AcpAgentOptions;
|
|
34
|
+
export declare function optionsToConfigMap(options: AcpAgentOptions): JsonMap;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 ACP Project
|
|
3
|
+
* Licensed under the Apache License, Version 2.0
|
|
4
|
+
* See LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
export function defaultAgentOptions() {
|
|
7
|
+
return {
|
|
8
|
+
storage_dir: ".acp-data",
|
|
9
|
+
relay_url: "https://localhost:8080",
|
|
10
|
+
relay_hints: [],
|
|
11
|
+
enterprise_directory_hints: [],
|
|
12
|
+
discovery_scheme: "https",
|
|
13
|
+
trust_profile: "self_asserted",
|
|
14
|
+
default_delivery_mode: "auto",
|
|
15
|
+
http_timeout_seconds: 10,
|
|
16
|
+
allow_insecure_http: false,
|
|
17
|
+
allow_insecure_tls: false,
|
|
18
|
+
mtls_enabled: false,
|
|
19
|
+
key_provider: "local",
|
|
20
|
+
vault_token_env: "VAULT_TOKEN",
|
|
21
|
+
amqp_exchange: "acp.exchange",
|
|
22
|
+
amqp_exchange_type: "direct",
|
|
23
|
+
mqtt_qos: 1,
|
|
24
|
+
mqtt_topic_prefix: "acp/agent",
|
|
25
|
+
extra: {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function asBoolean(value, fallback) {
|
|
29
|
+
if (typeof value === "boolean") {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
const normalized = value.trim().toLowerCase();
|
|
34
|
+
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
function asString(value) {
|
|
44
|
+
if (typeof value !== "string") {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const normalized = value.trim();
|
|
48
|
+
return normalized ? normalized : undefined;
|
|
49
|
+
}
|
|
50
|
+
function asNumber(value) {
|
|
51
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "string") {
|
|
55
|
+
const parsed = Number(value);
|
|
56
|
+
if (Number.isFinite(parsed)) {
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
export function optionsFromConfigMap(config) {
|
|
63
|
+
const options = defaultAgentOptions();
|
|
64
|
+
if (!config) {
|
|
65
|
+
return options;
|
|
66
|
+
}
|
|
67
|
+
options.allow_insecure_http = asBoolean(config.allow_insecure_http, false);
|
|
68
|
+
options.allow_insecure_tls = asBoolean(config.allow_insecure_tls, false);
|
|
69
|
+
options.mtls_enabled = asBoolean(config.mtls_enabled, false);
|
|
70
|
+
options.ca_file = asString(config.ca_file);
|
|
71
|
+
options.cert_file = asString(config.cert_file);
|
|
72
|
+
options.key_file = asString(config.key_file);
|
|
73
|
+
options.key_provider = asString(config.key_provider) ?? "local";
|
|
74
|
+
options.vault_url = asString(config.vault_url);
|
|
75
|
+
options.vault_path = asString(config.vault_path);
|
|
76
|
+
options.vault_token_env = asString(config.vault_token_env) ?? "VAULT_TOKEN";
|
|
77
|
+
options.endpoint = asString(config.endpoint);
|
|
78
|
+
options.relay_url = asString(config.relay_url) ?? options.relay_url;
|
|
79
|
+
options.discovery_scheme = asString(config.discovery_scheme) ?? options.discovery_scheme;
|
|
80
|
+
options.storage_dir = asString(config.storage_dir) ?? options.storage_dir;
|
|
81
|
+
options.vault_token = asString(config.vault_token);
|
|
82
|
+
options.amqp_broker_url = asString(config.amqp_broker_url);
|
|
83
|
+
options.amqp_exchange = asString(config.amqp_exchange) ?? options.amqp_exchange;
|
|
84
|
+
options.amqp_exchange_type = asString(config.amqp_exchange_type) ?? options.amqp_exchange_type;
|
|
85
|
+
options.mqtt_broker_url = asString(config.mqtt_broker_url);
|
|
86
|
+
options.mqtt_topic_prefix = asString(config.mqtt_topic_prefix) ?? options.mqtt_topic_prefix;
|
|
87
|
+
options.mqtt_qos = Math.min(2, Math.max(0, asNumber(config.mqtt_qos) ?? options.mqtt_qos));
|
|
88
|
+
if (Array.isArray(config.relay_hints)) {
|
|
89
|
+
options.relay_hints = config.relay_hints.filter((item) => typeof item === "string");
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(config.enterprise_directory_hints)) {
|
|
92
|
+
options.enterprise_directory_hints = config.enterprise_directory_hints.filter((item) => typeof item === "string");
|
|
93
|
+
}
|
|
94
|
+
return options;
|
|
95
|
+
}
|
|
96
|
+
export function optionsToConfigMap(options) {
|
|
97
|
+
return {
|
|
98
|
+
allow_insecure_http: options.allow_insecure_http,
|
|
99
|
+
allow_insecure_tls: options.allow_insecure_tls,
|
|
100
|
+
mtls_enabled: options.mtls_enabled,
|
|
101
|
+
ca_file: options.ca_file ?? null,
|
|
102
|
+
cert_file: options.cert_file ?? null,
|
|
103
|
+
key_file: options.key_file ?? null,
|
|
104
|
+
key_provider: options.key_provider,
|
|
105
|
+
vault_url: options.vault_url ?? null,
|
|
106
|
+
vault_path: options.vault_path ?? null,
|
|
107
|
+
vault_token_env: options.vault_token_env
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AcpAgent } from "./agent.js";
|
|
2
|
+
import { JsonMap } from "./jsonSupport.js";
|
|
3
|
+
import { DeliveryMode } from "./messages.js";
|
|
4
|
+
export interface OverlayTarget {
|
|
5
|
+
agent_id: string;
|
|
6
|
+
base_url: string;
|
|
7
|
+
well_known_url: string;
|
|
8
|
+
identity_document_url: string;
|
|
9
|
+
}
|
|
10
|
+
export interface OverlaySendResult {
|
|
11
|
+
target?: OverlayTarget;
|
|
12
|
+
send_result: JsonMap;
|
|
13
|
+
}
|
|
14
|
+
export type BusinessHandler = (payload: JsonMap) => JsonMap | undefined;
|
|
15
|
+
export type PassthroughHandler = (payload: JsonMap) => JsonMap | undefined;
|
|
16
|
+
export declare function isAcpHttpMessage(body: JsonMap): boolean;
|
|
17
|
+
export declare function invalidOverlayRequest(detail: string): JsonMap;
|
|
18
|
+
export declare class OverlayInboundAdapter {
|
|
19
|
+
agent: AcpAgent;
|
|
20
|
+
private readonly businessHandler;
|
|
21
|
+
private readonly passthroughHandler?;
|
|
22
|
+
constructor(agent: AcpAgent, businessHandler: BusinessHandler, passthroughHandler?: PassthroughHandler | undefined);
|
|
23
|
+
handleRequest(body: JsonMap): Promise<JsonMap>;
|
|
24
|
+
}
|
|
25
|
+
export declare class OverlayOutboundAdapter {
|
|
26
|
+
agent: AcpAgent;
|
|
27
|
+
constructor(agent: AcpAgent);
|
|
28
|
+
resolveTarget(targetBaseUrl: string, expectedAgentId?: string): Promise<OverlayTarget>;
|
|
29
|
+
sendBusinessPayload(input: {
|
|
30
|
+
payload: JsonMap;
|
|
31
|
+
target_base_url?: string;
|
|
32
|
+
recipient_agent_id?: string;
|
|
33
|
+
context?: string;
|
|
34
|
+
delivery_mode?: DeliveryMode;
|
|
35
|
+
expires_in_seconds?: number;
|
|
36
|
+
}): Promise<OverlaySendResult>;
|
|
37
|
+
}
|
package/dist/overlay.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 ACP Project
|
|
3
|
+
* Licensed under the Apache License, Version 2.0
|
|
4
|
+
* See LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { toJsonMap } from "./jsonSupport.js";
|
|
7
|
+
import { validationError } from "./errors.js";
|
|
8
|
+
export function isAcpHttpMessage(body) {
|
|
9
|
+
return Boolean(body.envelope &&
|
|
10
|
+
typeof body.envelope === "object" &&
|
|
11
|
+
!Array.isArray(body.envelope) &&
|
|
12
|
+
body.protected &&
|
|
13
|
+
typeof body.protected === "object" &&
|
|
14
|
+
!Array.isArray(body.protected));
|
|
15
|
+
}
|
|
16
|
+
export function invalidOverlayRequest(detail) {
|
|
17
|
+
return {
|
|
18
|
+
mode: "invalid",
|
|
19
|
+
state: "FAILED",
|
|
20
|
+
reason_code: "POLICY_REJECTED",
|
|
21
|
+
detail,
|
|
22
|
+
response_message: null
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export class OverlayInboundAdapter {
|
|
26
|
+
agent;
|
|
27
|
+
businessHandler;
|
|
28
|
+
passthroughHandler;
|
|
29
|
+
constructor(agent, businessHandler, passthroughHandler) {
|
|
30
|
+
this.agent = agent;
|
|
31
|
+
this.businessHandler = businessHandler;
|
|
32
|
+
this.passthroughHandler = passthroughHandler;
|
|
33
|
+
}
|
|
34
|
+
async handleRequest(body) {
|
|
35
|
+
if (!isAcpHttpMessage(body)) {
|
|
36
|
+
if (this.passthroughHandler) {
|
|
37
|
+
return {
|
|
38
|
+
mode: "passthrough",
|
|
39
|
+
payload: this.passthroughHandler(body) ?? {}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
throw validationError("Request is not an ACP message and no passthrough_handler is configured");
|
|
43
|
+
}
|
|
44
|
+
const inbound = await this.agent.receive(body, (payload) => this.businessHandler(payload));
|
|
45
|
+
return {
|
|
46
|
+
mode: "acp",
|
|
47
|
+
acp_result: inbound,
|
|
48
|
+
state: inbound.state,
|
|
49
|
+
reason_code: inbound.reason_code ?? null,
|
|
50
|
+
detail: inbound.detail ?? null,
|
|
51
|
+
response_message: inbound.response_message ?? null
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class OverlayOutboundAdapter {
|
|
56
|
+
agent;
|
|
57
|
+
constructor(agent) {
|
|
58
|
+
this.agent = agent;
|
|
59
|
+
}
|
|
60
|
+
async resolveTarget(targetBaseUrl, expectedAgentId) {
|
|
61
|
+
const resolved = await this.agent.resolveWellKnown(targetBaseUrl, expectedAgentId);
|
|
62
|
+
const wellKnown = toJsonMap(resolved.well_known);
|
|
63
|
+
const identityDocument = toJsonMap(resolved.identity_document);
|
|
64
|
+
const agentId = identityDocument.agent_id;
|
|
65
|
+
if (typeof agentId !== "string" || !agentId.trim()) {
|
|
66
|
+
throw validationError("Resolved well-known metadata did not include a valid identity_document.agent_id");
|
|
67
|
+
}
|
|
68
|
+
const identityDocumentUrl = wellKnown.identity_document;
|
|
69
|
+
if (typeof identityDocumentUrl !== "string" || !identityDocumentUrl.trim()) {
|
|
70
|
+
throw validationError("Resolved well-known metadata did not include a valid identity_document URL");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
agent_id: agentId,
|
|
74
|
+
base_url: targetBaseUrl.replace(/\/+$/, ""),
|
|
75
|
+
well_known_url: typeof resolved.well_known_url === "string" ? resolved.well_known_url : "",
|
|
76
|
+
identity_document_url: identityDocumentUrl
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async sendBusinessPayload(input) {
|
|
80
|
+
let target;
|
|
81
|
+
let recipientAgentId = input.recipient_agent_id;
|
|
82
|
+
if (input.target_base_url) {
|
|
83
|
+
target = await this.resolveTarget(input.target_base_url, recipientAgentId);
|
|
84
|
+
recipientAgentId = recipientAgentId ?? target.agent_id;
|
|
85
|
+
}
|
|
86
|
+
if (!recipientAgentId) {
|
|
87
|
+
throw validationError("send_business_payload requires recipient_agent_id or target_base_url for well-known bootstrap");
|
|
88
|
+
}
|
|
89
|
+
const sendResult = await this.agent.send([recipientAgentId], input.payload, input.context, "SEND", input.expires_in_seconds ?? 300, undefined, undefined, input.delivery_mode);
|
|
90
|
+
return {
|
|
91
|
+
target,
|
|
92
|
+
send_result: sendResult
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AcpAgent } from "./agent.js";
|
|
2
|
+
import { JsonMap, JsonValue } from "./jsonSupport.js";
|
|
3
|
+
import { DeliveryMode } from "./messages.js";
|
|
4
|
+
import { BusinessHandler, OverlayInboundAdapter, OverlayOutboundAdapter, PassthroughHandler } from "./overlay.js";
|
|
5
|
+
export declare const WELL_KNOWN_CACHE_CONTROL = "public, max-age=300";
|
|
6
|
+
export interface OverlayHttpResponse {
|
|
7
|
+
status_code: number;
|
|
8
|
+
body: JsonMap;
|
|
9
|
+
}
|
|
10
|
+
export interface OverlayConfig {
|
|
11
|
+
agent: AcpAgent;
|
|
12
|
+
base_url: string;
|
|
13
|
+
passthrough_handler?: PassthroughHandler;
|
|
14
|
+
}
|
|
15
|
+
export declare class OverlayFrameworkRuntime {
|
|
16
|
+
readonly agent: AcpAgent;
|
|
17
|
+
readonly base_url: string;
|
|
18
|
+
readonly inbound_adapter: OverlayInboundAdapter;
|
|
19
|
+
readonly outbound_adapter: OverlayOutboundAdapter;
|
|
20
|
+
constructor(agent: AcpAgent, base_url: string, businessHandler: BusinessHandler, passthroughHandler?: PassthroughHandler);
|
|
21
|
+
static create(agent: AcpAgent, baseUrl: string, businessHandler: BusinessHandler, passthroughHandler?: PassthroughHandler): OverlayFrameworkRuntime;
|
|
22
|
+
handleMessageBody(body: JsonValue): Promise<OverlayHttpResponse>;
|
|
23
|
+
wellKnownDocument(): JsonMap;
|
|
24
|
+
static wellKnownHeaders(): JsonMap;
|
|
25
|
+
identityDocumentPayload(): JsonMap;
|
|
26
|
+
sendBusinessPayload(input: {
|
|
27
|
+
payload: JsonMap;
|
|
28
|
+
target_base_url?: string;
|
|
29
|
+
recipient_agent_id?: string;
|
|
30
|
+
context?: string;
|
|
31
|
+
delivery_mode?: DeliveryMode;
|
|
32
|
+
expires_in_seconds?: number;
|
|
33
|
+
}): Promise<JsonMap>;
|
|
34
|
+
sendAcp(targetUrl: string, payload: JsonMap, recipientAgentId?: string, context?: string, deliveryMode?: DeliveryMode, expiresInSeconds?: number): Promise<JsonMap>;
|
|
35
|
+
static handle(requestBody: JsonValue, businessHandler: BusinessHandler, config: OverlayConfig): Promise<OverlayHttpResponse>;
|
|
36
|
+
}
|
|
37
|
+
export declare class OverlayClient {
|
|
38
|
+
readonly agent: AcpAgent;
|
|
39
|
+
readonly outbound_adapter: OverlayOutboundAdapter;
|
|
40
|
+
constructor(agent: AcpAgent);
|
|
41
|
+
static create(agent: AcpAgent): OverlayClient;
|
|
42
|
+
sendAcp(targetUrl: string, payload: JsonMap, recipientAgentId?: string, context?: string, deliveryMode?: DeliveryMode, expiresInSeconds?: number): Promise<JsonMap>;
|
|
43
|
+
}
|
|
44
|
+
export declare function acpOverlayInbound(agent: AcpAgent, handler: BusinessHandler, passthrough?: boolean): (payload: JsonMap) => Promise<JsonMap>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 ACP Project
|
|
3
|
+
* Licensed under the Apache License, Version 2.0
|
|
4
|
+
* See LICENSE file for details.
|
|
5
|
+
*/
|
|
6
|
+
import { AcpError } from "./errors.js";
|
|
7
|
+
import { toJsonMap } from "./jsonSupport.js";
|
|
8
|
+
import { OverlayInboundAdapter, OverlayOutboundAdapter, invalidOverlayRequest } from "./overlay.js";
|
|
9
|
+
export const WELL_KNOWN_CACHE_CONTROL = "public, max-age=300";
|
|
10
|
+
function sendResultToMap(result) {
|
|
11
|
+
const output = {};
|
|
12
|
+
const target = result.target && typeof result.target === "object" && !Array.isArray(result.target)
|
|
13
|
+
? result.target
|
|
14
|
+
: undefined;
|
|
15
|
+
output.target = target ?? null;
|
|
16
|
+
output.send_result =
|
|
17
|
+
result.send_result && typeof result.send_result === "object" && !Array.isArray(result.send_result)
|
|
18
|
+
? result.send_result
|
|
19
|
+
: null;
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
export class OverlayFrameworkRuntime {
|
|
23
|
+
agent;
|
|
24
|
+
base_url;
|
|
25
|
+
inbound_adapter;
|
|
26
|
+
outbound_adapter;
|
|
27
|
+
constructor(agent, base_url, businessHandler, passthroughHandler) {
|
|
28
|
+
this.agent = agent;
|
|
29
|
+
this.base_url = base_url;
|
|
30
|
+
if (!base_url.trim()) {
|
|
31
|
+
throw new AcpError("VALIDATION", "base_url is required");
|
|
32
|
+
}
|
|
33
|
+
this.base_url = base_url.replace(/\/+$/, "");
|
|
34
|
+
this.inbound_adapter = new OverlayInboundAdapter(agent, businessHandler, passthroughHandler);
|
|
35
|
+
this.outbound_adapter = new OverlayOutboundAdapter(agent);
|
|
36
|
+
}
|
|
37
|
+
static create(agent, baseUrl, businessHandler, passthroughHandler) {
|
|
38
|
+
return new OverlayFrameworkRuntime(agent, baseUrl, businessHandler, passthroughHandler);
|
|
39
|
+
}
|
|
40
|
+
async handleMessageBody(body) {
|
|
41
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
42
|
+
return {
|
|
43
|
+
status_code: 400,
|
|
44
|
+
body: invalidOverlayRequest("Expected JSON object request body")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const response = await this.inbound_adapter.handleRequest(toJsonMap(body));
|
|
49
|
+
return { status_code: 200, body: response };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
status_code: 400,
|
|
54
|
+
body: invalidOverlayRequest(String(error))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
wellKnownDocument() {
|
|
59
|
+
return this.agent.buildWellKnownDocument(this.base_url);
|
|
60
|
+
}
|
|
61
|
+
static wellKnownHeaders() {
|
|
62
|
+
return {
|
|
63
|
+
"Cache-Control": WELL_KNOWN_CACHE_CONTROL
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
identityDocumentPayload() {
|
|
67
|
+
return {
|
|
68
|
+
identity_document: this.agent.identity_document
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async sendBusinessPayload(input) {
|
|
72
|
+
const result = await this.outbound_adapter.sendBusinessPayload(input);
|
|
73
|
+
return sendResultToMap(result);
|
|
74
|
+
}
|
|
75
|
+
async sendAcp(targetUrl, payload, recipientAgentId, context, deliveryMode = "auto", expiresInSeconds = 300) {
|
|
76
|
+
return this.sendBusinessPayload({
|
|
77
|
+
payload,
|
|
78
|
+
target_base_url: targetUrl,
|
|
79
|
+
recipient_agent_id: recipientAgentId,
|
|
80
|
+
context,
|
|
81
|
+
delivery_mode: deliveryMode,
|
|
82
|
+
expires_in_seconds: expiresInSeconds
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
static async handle(requestBody, businessHandler, config) {
|
|
86
|
+
try {
|
|
87
|
+
const runtime = new OverlayFrameworkRuntime(config.agent, config.base_url, businessHandler, config.passthrough_handler);
|
|
88
|
+
return runtime.handleMessageBody(requestBody);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
status_code: 400,
|
|
93
|
+
body: invalidOverlayRequest(String(error))
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class OverlayClient {
|
|
99
|
+
agent;
|
|
100
|
+
outbound_adapter;
|
|
101
|
+
constructor(agent) {
|
|
102
|
+
this.agent = agent;
|
|
103
|
+
this.outbound_adapter = new OverlayOutboundAdapter(agent);
|
|
104
|
+
}
|
|
105
|
+
static create(agent) {
|
|
106
|
+
return new OverlayClient(agent);
|
|
107
|
+
}
|
|
108
|
+
async sendAcp(targetUrl, payload, recipientAgentId, context, deliveryMode = "auto", expiresInSeconds = 300) {
|
|
109
|
+
const result = await this.outbound_adapter.sendBusinessPayload({
|
|
110
|
+
payload,
|
|
111
|
+
target_base_url: targetUrl,
|
|
112
|
+
recipient_agent_id: recipientAgentId,
|
|
113
|
+
context,
|
|
114
|
+
delivery_mode: deliveryMode,
|
|
115
|
+
expires_in_seconds: expiresInSeconds
|
|
116
|
+
});
|
|
117
|
+
return sendResultToMap(result);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function acpOverlayInbound(agent, handler, passthrough = false) {
|
|
121
|
+
let currentAgent = agent;
|
|
122
|
+
const passthroughHandler = passthrough ? handler : undefined;
|
|
123
|
+
return async (payload) => {
|
|
124
|
+
const inbound = new OverlayInboundAdapter(currentAgent, handler, passthroughHandler);
|
|
125
|
+
const response = await inbound.handleRequest(payload);
|
|
126
|
+
currentAgent = inbound.agent;
|
|
127
|
+
return response;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AcpMessage } from "./messages.js";
|
|
2
|
+
import { HttpSecurityPolicy } from "./httpSecurity.js";
|
|
3
|
+
import { JsonMap } from "./jsonSupport.js";
|
|
4
|
+
export interface TransportResponse {
|
|
5
|
+
status_code: number;
|
|
6
|
+
body?: JsonMap;
|
|
7
|
+
raw_body: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class TransportClient {
|
|
10
|
+
private readonly timeoutSeconds;
|
|
11
|
+
private readonly policy;
|
|
12
|
+
private readonly fetchOptions;
|
|
13
|
+
constructor(timeoutSeconds: number, policy: HttpSecurityPolicy);
|
|
14
|
+
postJson(url: string, body: JsonMap): Promise<TransportResponse>;
|
|
15
|
+
sendToRelay(relayUrl: string, message: AcpMessage): Promise<JsonMap>;
|
|
16
|
+
}
|