agentschat-mcp 0.33.7 → 0.34.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/CHANGELOG.md +81 -0
- package/README.md +93 -52
- package/connector/README.md +181 -121
- package/connector/identities.ts +40 -0
- package/connector/ingest.ts +6 -2
- package/connector/run.ts +171 -45
- package/connector/server.ts +174 -104
- package/dist/connector.js +373 -152
- package/dist/server.js +91 -35
- package/package.json +3 -2
- package/skills/onboarding.md +236 -39
- package/src/cli.mjs +67 -23
- package/src/identity.ts +12 -0
- package/src/server.ts +75 -31
package/connector/server.ts
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* AgentsChat relay connector. Repeated hello frames register identities on a
|
|
3
|
+
* connection authenticated with a gatewayId AND a specific signing secret.
|
|
4
|
+
* Every identity must match both credentials; outbound additionally requires
|
|
5
|
+
* that identity's hello on the sending connection. There is no global fallback
|
|
6
|
+
* and no chat-sticky identity inference.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Frame exchange (see gateway ws_transport.py):
|
|
10
|
-
* gateway → hello {type:"hello", platform, botId}
|
|
11
|
-
* connector→ descriptor {type:"descriptor", descriptor:{...}} (one per hello)
|
|
12
|
-
* connector→ inbound {type:"inbound", event:{...}} (agentschat → gateway)
|
|
13
|
-
* gateway → outbound {type:"outbound", requestId, platform?, action}
|
|
14
|
-
* connector→ outbound_result {type:"outbound_result", requestId, result}
|
|
15
|
-
*
|
|
16
|
-
* Auth is fail-closed: anything wrong with the upgrade token closes 4401 before the
|
|
17
|
-
* socket is admitted.
|
|
18
|
-
*
|
|
19
|
-
* The single highest-correctness invariant (multiplex): identity A's messages are
|
|
20
|
-
* NEVER routed to or sent as identity B. Inbound routes to the socket(s) fronting the
|
|
21
|
-
* addressed identity; outbound uses the sending identity's own token; an identity the
|
|
22
|
-
* connector has no credentials for is rejected at hello (fail closed).
|
|
8
|
+
* Use separate single-identity gateways for Hermes transports that stamp the
|
|
9
|
+
* first botId for a platform on all outbound actions. A shared multi-hello WS
|
|
10
|
+
* requires a client that supplies the correct explicit outbound botId.
|
|
11
|
+
* All connector frames are newline-terminated (relay wire contract v1).
|
|
23
12
|
*/
|
|
24
13
|
|
|
25
14
|
import { createServer, type Server as HttpServer, type IncomingMessage } from "node:http";
|
|
@@ -27,7 +16,8 @@ import { WebSocketServer, WebSocket } from "ws";
|
|
|
27
16
|
import { verifyUpgradeToken, CLOSE_UNAUTHORIZED } from "./auth.ts";
|
|
28
17
|
import { buildDescriptor, type CapabilityDescriptor } from "./descriptor.ts";
|
|
29
18
|
import { toWireEvent, type AgentsChatMessage } from "./normalize.ts";
|
|
30
|
-
import {
|
|
19
|
+
import { MessageDedup } from "../src/dedup.ts";
|
|
20
|
+
import { IdentityTable, routeInboundTargets, hermesSourceProfile, type Identity } from "./identities.ts";
|
|
31
21
|
|
|
32
22
|
/** What the connector needs from agentschat to fulfil outbound ops, per identity. */
|
|
33
23
|
export interface AgentsChatHooks {
|
|
@@ -43,7 +33,15 @@ export interface AgentsChatHooks {
|
|
|
43
33
|
* [] when there is nothing worth attaching. Best-effort: failures must not
|
|
44
34
|
* block delivery of the addressed message itself.
|
|
45
35
|
*/
|
|
46
|
-
getChannelContext?(botId: string, chatId: string, sinceTs?: string, excludeId?: string): Promise<Array<{ text: string; user_name?: string; user_id?: string }> | null>;
|
|
36
|
+
getChannelContext?(botId: string, chatId: string, sinceTs?: string, excludeId?: string, signal?: AbortSignal): Promise<Array<{ text: string; user_name?: string; user_id?: string }> | null>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Only a structured status may be exposed, never a response body. */
|
|
40
|
+
export class PlatformHttpError extends Error {
|
|
41
|
+
constructor(readonly status: number) { super("platform HTTP failure"); }
|
|
42
|
+
}
|
|
43
|
+
export class PlatformNetworkError extends Error {
|
|
44
|
+
constructor() { super("platform network failure"); }
|
|
47
45
|
}
|
|
48
46
|
|
|
49
47
|
/** Legacy single-tenant hook shape (no botId first arg) — adapted to the per-identity one. */
|
|
@@ -73,16 +71,24 @@ export interface ConnectorConfig {
|
|
|
73
71
|
|
|
74
72
|
export interface ConnectorHandle {
|
|
75
73
|
port: number;
|
|
74
|
+
ready: Promise<void>;
|
|
76
75
|
stop(): void;
|
|
77
76
|
/** Push an agentschat message to the gateway socket(s) fronting its addressed identity. */
|
|
78
77
|
injectAgentsChatMessage(msg: AgentsChatMessage): void | Promise<void>;
|
|
79
78
|
connections(): number;
|
|
79
|
+
/**
|
|
80
|
+
* Hot-replace the identity table (and upgrade-auth secrets derived from it).
|
|
81
|
+
* Used when RELAY_IDENTITIES is reloaded without restarting the process.
|
|
82
|
+
*/
|
|
83
|
+
reloadIdentities(identities: Identity[]): void;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/** A connected gateway socket and the set of identities it has hello'd (fronts). */
|
|
83
87
|
interface GatewayConn {
|
|
84
88
|
ws: WebSocket;
|
|
85
89
|
gatewayId: string;
|
|
90
|
+
secret: string;
|
|
91
|
+
legacyAlias?: string;
|
|
86
92
|
/** botIds this socket has declared via hello. */
|
|
87
93
|
fronted: Set<string>;
|
|
88
94
|
}
|
|
@@ -90,16 +96,18 @@ interface GatewayConn {
|
|
|
90
96
|
export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
91
97
|
const log = config.logger ?? (() => {});
|
|
92
98
|
const descriptor = buildDescriptor(config.descriptor);
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// table holds exactly one identity (legacy OR a one-entry RELAY_IDENTITIES) —
|
|
96
|
-
// it gates hello acceptance and outbound identity resolution, NOT inbound
|
|
97
|
-
// group forwarding (unaddressed group chatter is dropped in every mode).
|
|
99
|
+
// No identity table: legacy single-tenant embedding with chatId-first hooks.
|
|
100
|
+
// Configured tables, including N=1, require an exact authenticated botId.
|
|
98
101
|
const legacy = !config.identities || config.identities.length === 0;
|
|
99
102
|
const table = new IdentityTable(
|
|
100
103
|
legacy ? [{ botId: "default", agentId: "default", token: "", gatewayId: "", secret: "" }] : config.identities!,
|
|
101
104
|
);
|
|
102
|
-
|
|
105
|
+
function safeLabel(value: string) {
|
|
106
|
+
for (const id of table.all()) for (const secret of [id.token, id.secret]) {
|
|
107
|
+
if (secret) value = value.split(secret).join("[redacted]");
|
|
108
|
+
}
|
|
109
|
+
return JSON.stringify(value.slice(0, 128));
|
|
110
|
+
}
|
|
103
111
|
// Normalize hooks to the per-identity shape. Legacy single-tenant hooks take
|
|
104
112
|
// (chatId, ...); wrap them to ignore the botId. Per-identity hooks take botId first.
|
|
105
113
|
const hooks: AgentsChatHooks = legacy
|
|
@@ -112,10 +120,11 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
112
120
|
}
|
|
113
121
|
: (config.agentschat as AgentsChatHooks);
|
|
114
122
|
const sockets = new Set<GatewayConn>();
|
|
115
|
-
//
|
|
123
|
+
// JSON [botId, chatId] → timestamp of the last message ADDRESSED to that
|
|
116
124
|
// identity in that channel. Drives the getChannelContext `sinceTs` window.
|
|
117
125
|
const lastAddressed = new Map<string, string>();
|
|
118
|
-
|
|
126
|
+
const delivered = new MessageDedup();
|
|
127
|
+
const inboundQueues = new Map<string, Promise<void>>();
|
|
119
128
|
const http: HttpServer = createServer((_req, res) => {
|
|
120
129
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
121
130
|
res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1, identities: table.size }));
|
|
@@ -133,7 +142,8 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
133
142
|
const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
|
|
134
143
|
const payload = peekPayload(token);
|
|
135
144
|
const secrets = payload ? config.secrets[payload] : undefined;
|
|
136
|
-
const
|
|
145
|
+
const secret = secrets?.find((s) => verifyUpgradeToken(token, [s]) === payload);
|
|
146
|
+
const gatewayId = secret ? payload : null;
|
|
137
147
|
if (!gatewayId) {
|
|
138
148
|
log(`[connector] rejecting upgrade: bad/absent token (path=${pathname})`);
|
|
139
149
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -142,14 +152,14 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
142
152
|
return;
|
|
143
153
|
}
|
|
144
154
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
145
|
-
onConnection(ws, gatewayId);
|
|
155
|
+
onConnection(ws, gatewayId, secret!);
|
|
146
156
|
});
|
|
147
157
|
});
|
|
148
158
|
|
|
149
|
-
function onConnection(ws: WebSocket, gatewayId: string) {
|
|
150
|
-
const conn: GatewayConn = { ws, gatewayId, fronted: new Set() };
|
|
159
|
+
function onConnection(ws: WebSocket, gatewayId: string, secret: string) {
|
|
160
|
+
const conn: GatewayConn = { ws, gatewayId, secret, fronted: new Set() };
|
|
151
161
|
sockets.add(conn);
|
|
152
|
-
log(`[connector] gateway connected: ${gatewayId} (${sockets.size} total)`);
|
|
162
|
+
log(`[connector] gateway connected: ${safeLabel(gatewayId)} (${sockets.size} total)`);
|
|
153
163
|
|
|
154
164
|
ws.on("message", async (data) => {
|
|
155
165
|
// Newline-delimited: the gateway may batch frames; split and handle each.
|
|
@@ -167,7 +177,7 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
167
177
|
});
|
|
168
178
|
ws.on("close", () => {
|
|
169
179
|
sockets.delete(conn);
|
|
170
|
-
log(`[connector] gateway disconnected: ${gatewayId} (${sockets.size} left)`);
|
|
180
|
+
log(`[connector] gateway disconnected: ${safeLabel(gatewayId)} (${sockets.size} left)`);
|
|
171
181
|
});
|
|
172
182
|
ws.on("error", () => sockets.delete(conn));
|
|
173
183
|
}
|
|
@@ -179,21 +189,39 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
179
189
|
// Single-tenant (no identity table configured): front whatever identity the
|
|
180
190
|
// gateway declares — there's exactly one. Multiplex: the botId MUST be a
|
|
181
191
|
// registered identity (fail closed — never front an identity we can't send as).
|
|
182
|
-
const identity =
|
|
183
|
-
|
|
184
|
-
:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
const identity = legacy ? table.all()[0] : table.forBot(botId);
|
|
193
|
+
const reason = frame.platform !== "agentschat" ? "unsupported_platform" : !identity ? "unknown_identity"
|
|
194
|
+
: !legacy && (identity.gatewayId !== conn.gatewayId || identity.secret !== conn.secret) ? "credential_mismatch" : null;
|
|
195
|
+
const labels = `gatewayId=${safeLabel(conn.gatewayId)} platform=${safeLabel(frame.platform === "agentschat" ? "agentschat" : "(unsupported)")} botId=${safeLabel(identity ? botId : "(unknown)")}`;
|
|
196
|
+
if (reason) {
|
|
197
|
+
log(`[connector] hello rejected ${labels} reason=${reason}`);
|
|
198
|
+
conn.fronted.clear();
|
|
199
|
+
send(conn.ws, { type: "error", error: `hello rejected: ${reason}` });
|
|
200
|
+
// Upgrade credentials were accepted; this hello does not match the
|
|
201
|
+
// configured identity/protocol. Fail closed, but allow configuration repair
|
|
202
|
+
// to recover through Hermes's normal reconnect path. Repeated 4401 after
|
|
203
|
+
// a prior descriptor latches credential revocation in ws_transport.py.
|
|
204
|
+
conn.ws.close(1002, `hello rejected: ${reason}`);
|
|
189
205
|
return;
|
|
190
206
|
}
|
|
191
|
-
conn.fronted.add(identity
|
|
207
|
+
conn.fronted.add(identity!.botId);
|
|
208
|
+
if (legacy) conn.legacyAlias = botId;
|
|
209
|
+
log(`[connector] hello accepted ${labels}`);
|
|
192
210
|
send(conn.ws, { type: "descriptor", descriptor: { ...descriptor, platform: "agentschat" } });
|
|
193
211
|
return;
|
|
194
212
|
}
|
|
195
213
|
if (t === "outbound") {
|
|
196
|
-
|
|
214
|
+
let result;
|
|
215
|
+
try { result = await handleOutbound(conn, frame); }
|
|
216
|
+
catch (error) {
|
|
217
|
+
const status = error instanceof PlatformHttpError && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : undefined;
|
|
218
|
+
result = status !== undefined
|
|
219
|
+
? { success: false, error: `agentschat HTTP ${status}`, code: "platform_http_error", status, ambiguous: status >= 500 }
|
|
220
|
+
: error instanceof PlatformNetworkError
|
|
221
|
+
? { success: false, error: "agentschat network failure; outcome unknown", code: "platform_network_error", ambiguous: true }
|
|
222
|
+
: { success: false, error: "platform operation failed; outcome unknown", code: "platform_operation_failed", ambiguous: true };
|
|
223
|
+
log(`[connector] outbound failed: ${result.code}${status ? ` status=${status}` : ""}`);
|
|
224
|
+
}
|
|
197
225
|
send(conn.ws, { type: "outbound_result", requestId: frame.requestId, result });
|
|
198
226
|
return;
|
|
199
227
|
}
|
|
@@ -204,23 +232,16 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
204
232
|
const action = frame?.action ?? {};
|
|
205
233
|
const op = action?.op;
|
|
206
234
|
const chatId = action?.chat_id ?? "";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const firstFronted = [...conn.fronted][0];
|
|
215
|
-
const requested = typeof frame?.botId === "string" && frame.botId ? frame.botId : firstFronted ?? null;
|
|
216
|
-
const identity = single
|
|
217
|
-
? table.all()[0]
|
|
218
|
-
: requested && conn.fronted.has(requested)
|
|
219
|
-
? table.forBot(requested)
|
|
220
|
-
: null;
|
|
235
|
+
const firstFronted = conn.fronted.size === 1 ? [...conn.fronted][0] : undefined;
|
|
236
|
+
let requested = frame.botId === undefined ? firstFronted ?? null
|
|
237
|
+
: typeof frame.botId === "string" && frame.botId ? frame.botId : null;
|
|
238
|
+
if (legacy && requested && requested === conn.legacyAlias) requested = "default";
|
|
239
|
+
let identity = requested && conn.fronted.has(requested) ? table.forBot(requested) : null;
|
|
240
|
+
if (identity && ((!legacy && (identity.gatewayId !== conn.gatewayId || identity.secret !== conn.secret)) ||
|
|
241
|
+
(frame.platform !== undefined && frame.platform !== "agentschat"))) identity = null;
|
|
221
242
|
if (!identity) {
|
|
222
|
-
log(`[connector] outbound failed: no
|
|
223
|
-
return { success: false, error: `no
|
|
243
|
+
log(`[connector] outbound failed: no usable identity for botId=${requested ?? "?"}`);
|
|
244
|
+
return { success: false, error: `no usable identity for outbound (botId=${requested ?? "?"})` };
|
|
224
245
|
}
|
|
225
246
|
switch (op) {
|
|
226
247
|
case "send": {
|
|
@@ -247,12 +268,18 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
247
268
|
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj) + "\n");
|
|
248
269
|
}
|
|
249
270
|
|
|
271
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
272
|
+
http.once("listening", resolve);
|
|
273
|
+
http.once("error", reject);
|
|
274
|
+
});
|
|
250
275
|
http.listen(config.port, config.host ?? "127.0.0.1");
|
|
251
|
-
const address = http.address();
|
|
252
|
-
const port = typeof address === "object" && address ? address.port : config.port;
|
|
253
276
|
|
|
254
277
|
return {
|
|
255
|
-
|
|
278
|
+
ready,
|
|
279
|
+
get port() {
|
|
280
|
+
const address = http.address();
|
|
281
|
+
return typeof address === "object" && address ? address.port : config.port;
|
|
282
|
+
},
|
|
256
283
|
stop() {
|
|
257
284
|
for (const c of sockets) {
|
|
258
285
|
try { c.ws.close(1001, "connector shutdown"); } catch {}
|
|
@@ -273,59 +300,102 @@ export function startConnector(config: ConnectorConfig): ConnectorHandle {
|
|
|
273
300
|
// unaddressed is dropped — injecting joined-channel chatter into the
|
|
274
301
|
// agent's session would burn its tokens on messages not meant for it.
|
|
275
302
|
const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
?
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}) ?? (table.isSingle() ? table.all()[0] : null)
|
|
282
|
-
: routeInbound(table, {
|
|
303
|
+
const owner = (msg as any).__botId ?? (msg as any).dm_owner;
|
|
304
|
+
const targets = isDm
|
|
305
|
+
? [owner !== undefined ? (typeof owner === "string" ? table.forBot(owner) : null)
|
|
306
|
+
: table.isSingle() ? table.all()[0] : null].filter((id): id is Identity => !!id)
|
|
307
|
+
: routeInboundTargets(table, {
|
|
283
308
|
channel_id: msg.channel_id,
|
|
284
309
|
mentioned_ids: (msg as any).mentioned_ids,
|
|
285
310
|
content: msg.content,
|
|
286
311
|
});
|
|
287
|
-
if (!
|
|
312
|
+
if (!targets.length) {
|
|
288
313
|
log(`[connector] inbound unaddressed (channel=${(msg as any).channel_id ?? "?"} mentions=${JSON.stringify((msg as any).mentioned_ids ?? [])}) — dropped`);
|
|
289
314
|
return;
|
|
290
315
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
316
|
+
await Promise.all(targets.map(target => {
|
|
317
|
+
const key = JSON.stringify([target.botId, msg.channel_id]);
|
|
318
|
+
const previous = inboundQueues.get(key) ?? Promise.resolve();
|
|
319
|
+
const job = previous.catch(() => {}).then(async () => {
|
|
320
|
+
if (msg.sender_id === target.agentId) return;
|
|
321
|
+
const baseEvent = toWireEvent(msg, "agentschat");
|
|
322
|
+
if (!baseEvent) return;
|
|
323
|
+
if (![...sockets].some(c => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId))) return;
|
|
324
|
+
const deliveryKey = typeof msg.id === "string" && msg.id
|
|
325
|
+
? JSON.stringify([msg.channel_id, msg.id, target.botId]) : null;
|
|
326
|
+
// Reserve synchronously, before async context fetch, across mirrored sockets.
|
|
327
|
+
if (deliveryKey && delivered.recordOrSkip(deliveryKey)) return;
|
|
328
|
+
if (!isDm) {
|
|
329
|
+
// Attach "what happened since you were last addressed" so the agent gets
|
|
330
|
+
// the conversation BETWEEN its @-mentions without being injected into
|
|
331
|
+
// every unaddressed message. Upstream renders event.context into the
|
|
332
|
+
// event's channel_context ("[Recent channel messages]") — gateway needs
|
|
333
|
+
// no change. Fetch errors fall back to delivering without context.
|
|
334
|
+
const since = lastAddressed.get(key);
|
|
335
|
+
if (hooks.getChannelContext) {
|
|
336
|
+
const controller = new AbortController();
|
|
337
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
338
|
+
try {
|
|
339
|
+
const ctx = await Promise.race([
|
|
340
|
+
hooks.getChannelContext(target.botId, msg.channel_id!, since, msg.id, controller.signal),
|
|
341
|
+
new Promise<null>(resolve => { timer = setTimeout(() => { controller.abort(); resolve(null); }, 1000); }),
|
|
342
|
+
]);
|
|
343
|
+
if (ctx && ctx.length) {
|
|
344
|
+
baseEvent.context = ctx.slice(-10).map((c) => ({
|
|
345
|
+
text: String(c?.text ?? "").slice(0, 500),
|
|
346
|
+
source: { user_name: c?.user_name, user_id: c?.user_id },
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
log(`[connector] context fetch failed (delivering without it)`);
|
|
351
|
+
} finally {
|
|
352
|
+
if (timer) clearTimeout(timer);
|
|
353
|
+
}
|
|
309
354
|
}
|
|
310
|
-
|
|
311
|
-
|
|
355
|
+
if (msg.timestamp && Number.isFinite(Date.parse(msg.timestamp)) &&
|
|
356
|
+
(!since || Date.parse(msg.timestamp) > Date.parse(since))) lastAddressed.set(key, msg.timestamp);
|
|
312
357
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
358
|
+
const deliverTo = [...sockets].filter((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)).slice(0, 1);
|
|
359
|
+
if (deliverTo.length === 0) {
|
|
360
|
+
log(`[connector] inbound dropped for botId=${target.botId}: no agentschat-fronted gateway socket`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
for (const conn of deliverTo) {
|
|
364
|
+
// Per-connection clone: a single-hello gateway must NOT inherit a
|
|
365
|
+
// profile stamp meant for a multiplexed sibling socket.
|
|
366
|
+
const event = { ...baseEvent, source: { ...baseEvent.source } };
|
|
367
|
+
const profile = hermesSourceProfile(target, conn.fronted.size);
|
|
368
|
+
if (profile) (event.source as any).profile = profile;
|
|
369
|
+
send(conn.ws, { type: "inbound", event });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
inboundQueues.set(key, job);
|
|
373
|
+
return job.finally(() => {
|
|
374
|
+
if (inboundQueues.get(key) === job) inboundQueues.delete(key);
|
|
375
|
+
});
|
|
376
|
+
}));
|
|
325
377
|
},
|
|
326
378
|
connections() {
|
|
327
379
|
return sockets.size;
|
|
328
380
|
},
|
|
381
|
+
reloadIdentities(next: Identity[]) {
|
|
382
|
+
if (!next || next.length === 0) {
|
|
383
|
+
throw new Error("reloadIdentities requires a non-empty identity list");
|
|
384
|
+
}
|
|
385
|
+
table.replace(next);
|
|
386
|
+
for (const conn of sockets) {
|
|
387
|
+
for (const botId of conn.fronted) {
|
|
388
|
+
const id = table.forBot(botId);
|
|
389
|
+
if (!id || id.gatewayId !== conn.gatewayId || id.secret !== conn.secret) conn.fronted.delete(botId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Rebuild upgrade-auth secrets in place so the upgrade handler sees them.
|
|
393
|
+
for (const k of Object.keys(config.secrets)) delete config.secrets[k];
|
|
394
|
+
for (const id of next) {
|
|
395
|
+
(config.secrets[id.gatewayId] ??= []).push(id.secret);
|
|
396
|
+
}
|
|
397
|
+
log(`[connector] identities reloaded: ${table.size}`);
|
|
398
|
+
},
|
|
329
399
|
};
|
|
330
400
|
}
|
|
331
401
|
|