@pcircle/memesh 4.7.3 → 4.8.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +3 -1
- package/README.de.md +42 -16
- package/README.md +46 -19
- package/README.zh-TW.md +42 -17
- package/dashboard/dist/index.html +1 -1
- package/dist/core/agent-message-storage.d.ts +76 -0
- package/dist/core/agent-message-storage.d.ts.map +1 -0
- package/dist/core/agent-message-storage.js +359 -0
- package/dist/core/agent-message-storage.js.map +1 -0
- package/dist/core/agent-messaging.d.ts +224 -0
- package/dist/core/agent-messaging.d.ts.map +1 -0
- package/dist/core/agent-messaging.js +881 -0
- package/dist/core/agent-messaging.js.map +1 -0
- package/dist/core/agent-router.d.ts +194 -0
- package/dist/core/agent-router.d.ts.map +1 -0
- package/dist/core/agent-router.js +1138 -0
- package/dist/core/agent-router.js.map +1 -0
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +8 -1
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/doctor.d.ts +19 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +176 -2
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/dreamer.d.ts +8 -5
- package/dist/core/dreamer.d.ts.map +1 -1
- package/dist/core/dreamer.js +88 -3
- package/dist/core/dreamer.js.map +1 -1
- package/dist/core/product-improvements.d.ts +61 -0
- package/dist/core/product-improvements.d.ts.map +1 -0
- package/dist/core/product-improvements.js +216 -0
- package/dist/core/product-improvements.js.map +1 -0
- package/dist/core/repo-state.d.ts +11 -0
- package/dist/core/repo-state.d.ts.map +1 -0
- package/dist/core/repo-state.js +70 -0
- package/dist/core/repo-state.js.map +1 -0
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +70 -0
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/task-state.d.ts.map +1 -1
- package/dist/core/task-state.js +3 -3
- package/dist/core/task-state.js.map +1 -1
- package/dist/core/updater.d.ts +2 -0
- package/dist/core/updater.d.ts.map +1 -1
- package/dist/core/updater.js +11 -5
- package/dist/core/updater.js.map +1 -1
- package/dist/core/work-topology.d.ts.map +1 -1
- package/dist/core/work-topology.js +1 -0
- package/dist/core/work-topology.js.map +1 -1
- package/dist/host-adapters/acp-client.d.ts +147 -0
- package/dist/host-adapters/acp-client.d.ts.map +1 -0
- package/dist/host-adapters/acp-client.js +793 -0
- package/dist/host-adapters/acp-client.js.map +1 -0
- package/dist/host-adapters/claude-channel.d.ts +75 -0
- package/dist/host-adapters/claude-channel.d.ts.map +1 -0
- package/dist/host-adapters/claude-channel.js +255 -0
- package/dist/host-adapters/claude-channel.js.map +1 -0
- package/dist/host-adapters/codex-app-server.d.ts +83 -0
- package/dist/host-adapters/codex-app-server.d.ts.map +1 -0
- package/dist/host-adapters/codex-app-server.js +360 -0
- package/dist/host-adapters/codex-app-server.js.map +1 -0
- package/dist/host-adapters/codex-cli-queue.d.ts +17 -0
- package/dist/host-adapters/codex-cli-queue.d.ts.map +1 -0
- package/dist/host-adapters/codex-cli-queue.js +85 -0
- package/dist/host-adapters/codex-cli-queue.js.map +1 -0
- package/dist/host-runtime/acp.d.ts +57 -0
- package/dist/host-runtime/acp.d.ts.map +1 -0
- package/dist/host-runtime/acp.js +338 -0
- package/dist/host-runtime/acp.js.map +1 -0
- package/dist/host-runtime/claude.d.ts +41 -0
- package/dist/host-runtime/claude.d.ts.map +1 -0
- package/dist/host-runtime/claude.js +213 -0
- package/dist/host-runtime/claude.js.map +1 -0
- package/dist/host-runtime/codex-session.d.ts +24 -0
- package/dist/host-runtime/codex-session.d.ts.map +1 -0
- package/dist/host-runtime/codex-session.js +98 -0
- package/dist/host-runtime/codex-session.js.map +1 -0
- package/dist/host-runtime/codex.d.ts +33 -0
- package/dist/host-runtime/codex.d.ts.map +1 -0
- package/dist/host-runtime/codex.js +215 -0
- package/dist/host-runtime/codex.js.map +1 -0
- package/dist/host-runtime/config.d.ts +9 -0
- package/dist/host-runtime/config.d.ts.map +1 -0
- package/dist/host-runtime/config.js +91 -0
- package/dist/host-runtime/config.js.map +1 -0
- package/dist/host-runtime/router-client.d.ts +38 -0
- package/dist/host-runtime/router-client.d.ts.map +1 -0
- package/dist/host-runtime/router-client.js +463 -0
- package/dist/host-runtime/router-client.js.map +1 -0
- package/dist/host-runtime/router.d.ts +3 -0
- package/dist/host-runtime/router.d.ts.map +1 -0
- package/dist/host-runtime/router.js +43 -0
- package/dist/host-runtime/router.js.map +1 -0
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/skills-manifest.json +22 -17
- package/dist/storage/conflicts.d.ts.map +1 -1
- package/dist/storage/conflicts.js +8 -1
- package/dist/storage/conflicts.js.map +1 -1
- package/dist/storage/schema.d.ts +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +255 -0
- package/dist/storage/schema.js.map +1 -1
- package/dist/transports/agent-messaging.d.ts +11 -0
- package/dist/transports/agent-messaging.d.ts.map +1 -0
- package/dist/transports/agent-messaging.js +273 -0
- package/dist/transports/agent-messaging.js.map +1 -0
- package/dist/transports/cli/cli.d.ts +6 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +392 -17
- package/dist/transports/cli/cli.js.map +1 -1
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +31 -2
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +152 -1
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +108 -3
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +106 -1
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +86 -0
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +167 -0
- package/docs/platforms/agent-messaging.md +290 -0
- package/hooks/hooks.json +11 -0
- package/llms-install.md +101 -25
- package/package.json +14 -5
- package/scripts/hooks/_generated/repo-state.js +77 -0
- package/scripts/hooks/_generated/schema.js +255 -0
- package/scripts/hooks/_generated/task-state.js +3 -3
- package/scripts/hooks/_generated/work-topology.js +1 -0
- package/scripts/hooks/_shared.js +77 -97
- package/scripts/hooks/auto-update-runner.mjs +243 -0
- package/scripts/hooks/session-start.js +12 -2
- package/scripts/hooks/session-summary.js +7 -6
- package/skills/memesh/SKILL.md +40 -1
|
@@ -0,0 +1,1138 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fetchAgentMessage, } from './agent-messaging.js';
|
|
6
|
+
export const AGENT_ROUTER_PROTOCOL_VERSION = 1;
|
|
7
|
+
export const AGENT_ROUTER_MAX_FRAME_BYTES = 64 * 1024;
|
|
8
|
+
export const AGENT_ROUTER_MAX_HOPS = 4;
|
|
9
|
+
const DEFAULT_RATE_LIMIT = 120;
|
|
10
|
+
const DEFAULT_RATE_WINDOW_MS = 60_000;
|
|
11
|
+
const DEFAULT_LEASE_MS = 60_000;
|
|
12
|
+
const MAX_LEASE_MS = 5 * 60_000;
|
|
13
|
+
const DEFAULT_DRAIN_LIMIT = 200;
|
|
14
|
+
const DEFAULT_CLIENT_TIMEOUT_MS = 2_000;
|
|
15
|
+
const STALE_SOCKET_PROBE_TIMEOUT_MS = 250;
|
|
16
|
+
const STARTUP_LOCK_WAIT_MS = 1_000;
|
|
17
|
+
const STARTUP_LOCK_STALE_MS = 5_000;
|
|
18
|
+
const MAX_FIELD_LENGTH = 200;
|
|
19
|
+
const MAX_ADAPTER_RECEIPT_BYTES = 16 * 1024;
|
|
20
|
+
export class AgentRouterError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class AgentRouterStaleGenerationError extends AgentRouterError {
|
|
28
|
+
constructor() {
|
|
29
|
+
super('stale_generation', 'The session connection generation is stale or inactive.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class AgentRouterProtocolError extends AgentRouterError {
|
|
33
|
+
}
|
|
34
|
+
export class AgentRouter {
|
|
35
|
+
router_instance_id;
|
|
36
|
+
socket_path;
|
|
37
|
+
db;
|
|
38
|
+
adapters;
|
|
39
|
+
limits;
|
|
40
|
+
server = null;
|
|
41
|
+
socketIdentity = null;
|
|
42
|
+
sockets = new Set();
|
|
43
|
+
externalConnections = new Map();
|
|
44
|
+
inFlightDeliveries = new Map();
|
|
45
|
+
pendingExternal = new Map();
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.db = options.db;
|
|
48
|
+
this.socket_path = validateSocketPath(options.socket_path);
|
|
49
|
+
this.router_instance_id = validateField('router_instance_id', options.router_instance_id ?? randomUUID());
|
|
50
|
+
this.limits = normalizeLimits(options.limits);
|
|
51
|
+
this.adapters = new Map();
|
|
52
|
+
for (const adapter of options.adapters) {
|
|
53
|
+
const kind = validateField('adapter kind', adapter.kind);
|
|
54
|
+
if (this.adapters.has(kind))
|
|
55
|
+
throw new AgentRouterError('duplicate_adapter', `Duplicate adapter ${kind}.`);
|
|
56
|
+
this.adapters.set(kind, adapter);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async start() {
|
|
60
|
+
if (this.server)
|
|
61
|
+
throw new AgentRouterError('already_started', 'The agent router is already started.');
|
|
62
|
+
if (process.platform === 'win32') {
|
|
63
|
+
throw new AgentRouterError('unsupported_secure_host_runtime', 'The secure local host runtime is not supported on Windows.');
|
|
64
|
+
}
|
|
65
|
+
const directory = path.dirname(this.socket_path);
|
|
66
|
+
if (!fs.existsSync(directory))
|
|
67
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
68
|
+
const directoryMode = fs.statSync(directory).mode & 0o777;
|
|
69
|
+
if ((directoryMode & 0o077) !== 0) {
|
|
70
|
+
throw new AgentRouterError('insecure_socket_directory', 'Router socket directory must be private.');
|
|
71
|
+
}
|
|
72
|
+
const startupLock = await acquireStartupLock(this.socket_path);
|
|
73
|
+
const server = net.createServer((socket) => this.acceptSocket(socket));
|
|
74
|
+
this.server = server;
|
|
75
|
+
try {
|
|
76
|
+
for (;;) {
|
|
77
|
+
try {
|
|
78
|
+
await listenOnSocket(server, this.socket_path);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code !== 'EADDRINUSE')
|
|
83
|
+
throw error;
|
|
84
|
+
if (!await removeOrphanedSocket(this.socket_path)) {
|
|
85
|
+
throw new AgentRouterError('router_already_running', 'A live or indeterminate router already owns the socket.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
fs.chmodSync(this.socket_path, 0o600);
|
|
90
|
+
this.socketIdentity = readSocketIdentity(this.socket_path);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.server = null;
|
|
94
|
+
this.socketIdentity = null;
|
|
95
|
+
try {
|
|
96
|
+
server.close();
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
releaseStartupLock(startupLock);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async stop() {
|
|
106
|
+
const server = this.server;
|
|
107
|
+
if (!server)
|
|
108
|
+
return;
|
|
109
|
+
this.server = null;
|
|
110
|
+
const socketIdentity = this.socketIdentity;
|
|
111
|
+
this.socketIdentity = null;
|
|
112
|
+
for (const socket of [...this.sockets]) {
|
|
113
|
+
this.disconnectSocket(socket);
|
|
114
|
+
socket.destroy();
|
|
115
|
+
}
|
|
116
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
117
|
+
if (socketIdentity)
|
|
118
|
+
unlinkSocketIfSame(this.socket_path, socketIdentity);
|
|
119
|
+
}
|
|
120
|
+
acceptSocket(socket) {
|
|
121
|
+
this.sockets.add(socket);
|
|
122
|
+
let pending = Buffer.alloc(0);
|
|
123
|
+
let windowStartedAt = Date.now();
|
|
124
|
+
let frameCount = 0;
|
|
125
|
+
socket.on('data', (chunk) => {
|
|
126
|
+
pending = Buffer.concat([pending, chunk]);
|
|
127
|
+
if (pending.length > this.limits.max_frame_bytes && !pending.includes(0x0a)) {
|
|
128
|
+
this.writeError(socket, '', 'frame_too_large', 'Router frame exceeds the byte limit.');
|
|
129
|
+
socket.destroy();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (;;) {
|
|
133
|
+
const newline = pending.indexOf(0x0a);
|
|
134
|
+
if (newline < 0)
|
|
135
|
+
break;
|
|
136
|
+
const frame = pending.subarray(0, newline);
|
|
137
|
+
pending = pending.subarray(newline + 1);
|
|
138
|
+
if (frame.length === 0)
|
|
139
|
+
continue;
|
|
140
|
+
if (frame.length > this.limits.max_frame_bytes) {
|
|
141
|
+
this.writeError(socket, '', 'frame_too_large', 'Router frame exceeds the byte limit.');
|
|
142
|
+
socket.destroy();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
if (now - windowStartedAt >= this.limits.rate_window_ms) {
|
|
147
|
+
windowStartedAt = now;
|
|
148
|
+
frameCount = 0;
|
|
149
|
+
}
|
|
150
|
+
frameCount += 1;
|
|
151
|
+
if (frameCount > this.limits.max_frames_per_window) {
|
|
152
|
+
this.writeError(socket, '', 'rate_limited', 'Router frame rate exceeded.');
|
|
153
|
+
socket.destroy();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
void this.handleFrame(socket, frame);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
socket.on('error', () => undefined);
|
|
160
|
+
socket.on('close', () => this.disconnectSocket(socket));
|
|
161
|
+
}
|
|
162
|
+
async handleFrame(socket, frame) {
|
|
163
|
+
let requestId = '';
|
|
164
|
+
try {
|
|
165
|
+
const request = parseRequest(frame, this.limits.max_hops);
|
|
166
|
+
requestId = request.request_id;
|
|
167
|
+
const result = await this.handleRequest(request, socket);
|
|
168
|
+
this.writeResponse(socket, { version: 1, request_id: requestId, ok: true, result });
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const routerError = toRouterError(error);
|
|
172
|
+
this.writeError(socket, requestId, routerError.code, routerError.message);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async handleRequest(request, socket) {
|
|
176
|
+
switch (request.type) {
|
|
177
|
+
case 'register':
|
|
178
|
+
return this.register(request, socket);
|
|
179
|
+
case 'notify': {
|
|
180
|
+
const delivered = await this.dispatchDelivery(request.delivery_id, request.project, request.hops + 1);
|
|
181
|
+
return { delivered };
|
|
182
|
+
}
|
|
183
|
+
case 'heartbeat': {
|
|
184
|
+
const connection = this.requireCurrentConnection(request);
|
|
185
|
+
this.db.transaction(() => {
|
|
186
|
+
const expiresAt = Date.now() + this.limits.lease_ms;
|
|
187
|
+
this.db.prepare(`
|
|
188
|
+
UPDATE agent_session_connections SET lease_expires_at_ms = ? WHERE connection_id = ?
|
|
189
|
+
`).run(expiresAt, connection.connection_id);
|
|
190
|
+
}).immediate();
|
|
191
|
+
return { generation: connection.generation, lease_ms: this.limits.lease_ms };
|
|
192
|
+
}
|
|
193
|
+
case 'disconnect': {
|
|
194
|
+
const connection = this.requireCurrentConnection(request);
|
|
195
|
+
this.db.transaction(() => {
|
|
196
|
+
this.db.prepare(`
|
|
197
|
+
UPDATE agent_session_connections
|
|
198
|
+
SET disconnected_at = CURRENT_TIMESTAMP, disconnect_reason = 'adapter_disconnect'
|
|
199
|
+
WHERE connection_id = ?
|
|
200
|
+
`).run(connection.connection_id);
|
|
201
|
+
this.insertPresenceFact(connection, 'disconnected', { reason: 'adapter_disconnect' });
|
|
202
|
+
}).immediate();
|
|
203
|
+
return { disconnected: true };
|
|
204
|
+
}
|
|
205
|
+
case 'host_accept':
|
|
206
|
+
case 'host_reject':
|
|
207
|
+
return this.handleHostOutcome(request, socket);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async register(request, socket) {
|
|
211
|
+
const adapter = this.adapters.get(request.adapter_kind);
|
|
212
|
+
if (!adapter)
|
|
213
|
+
throw new AgentRouterError('adapter_unavailable', 'The requested host adapter is unavailable.');
|
|
214
|
+
const registration = {
|
|
215
|
+
project: request.project,
|
|
216
|
+
principal_id: request.principal_id,
|
|
217
|
+
session_instance_id: request.session_instance_id,
|
|
218
|
+
adapter_kind: request.adapter_kind,
|
|
219
|
+
...(request.auth_token === undefined ? {} : { auth_token: request.auth_token }),
|
|
220
|
+
};
|
|
221
|
+
if (!await adapter.authenticate(registration)) {
|
|
222
|
+
throw new AgentRouterError('authentication_failed', 'The host adapter did not authenticate this session.');
|
|
223
|
+
}
|
|
224
|
+
const connection = this.registerConnection(registration);
|
|
225
|
+
this.bindExternalConnection(connection, socket);
|
|
226
|
+
setImmediate(() => { void this.drainConnection(connection, request.hops + 1).catch(() => undefined); });
|
|
227
|
+
return {
|
|
228
|
+
connection_id: connection.connection_id,
|
|
229
|
+
generation: connection.generation,
|
|
230
|
+
lease_ms: this.limits.lease_ms,
|
|
231
|
+
drain_scheduled: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
bindExternalConnection(connection, socket) {
|
|
235
|
+
for (const [connectionId, bound] of this.externalConnections) {
|
|
236
|
+
if (bound === socket)
|
|
237
|
+
this.externalConnections.delete(connectionId);
|
|
238
|
+
}
|
|
239
|
+
this.externalConnections.set(connection.connection_id, socket);
|
|
240
|
+
}
|
|
241
|
+
disconnectSocket(socket) {
|
|
242
|
+
this.sockets.delete(socket);
|
|
243
|
+
for (const [connectionId, bound] of this.externalConnections) {
|
|
244
|
+
if (bound !== socket)
|
|
245
|
+
continue;
|
|
246
|
+
this.externalConnections.delete(connectionId);
|
|
247
|
+
const row = this.db.prepare(`
|
|
248
|
+
SELECT connection_id, project, principal_id, session_instance_id, generation,
|
|
249
|
+
adapter_kind, router_instance_id, lease_expires_at_ms
|
|
250
|
+
FROM agent_session_connections WHERE connection_id = ? AND disconnected_at IS NULL
|
|
251
|
+
`).get(connectionId);
|
|
252
|
+
if (row) {
|
|
253
|
+
this.db.transaction(() => {
|
|
254
|
+
this.db.prepare(`UPDATE agent_session_connections
|
|
255
|
+
SET disconnected_at = CURRENT_TIMESTAMP, disconnect_reason = 'socket_closed'
|
|
256
|
+
WHERE connection_id = ? AND disconnected_at IS NULL`).run(connectionId);
|
|
257
|
+
this.insertPresenceFact(row, 'disconnected', { reason: 'socket_closed' });
|
|
258
|
+
}).immediate();
|
|
259
|
+
}
|
|
260
|
+
for (const [attemptId, pending] of this.pendingExternal) {
|
|
261
|
+
if (pending.connection.connection_id !== connectionId)
|
|
262
|
+
continue;
|
|
263
|
+
clearTimeout(pending.timer);
|
|
264
|
+
this.pendingExternal.delete(attemptId);
|
|
265
|
+
pending.resolve({ accepted: false, receipt: { failure_code: 'adapter_disconnected' } });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
handleHostOutcome(request, socket) {
|
|
270
|
+
const pending = this.pendingExternal.get(request.attempt_id);
|
|
271
|
+
if (!pending) {
|
|
272
|
+
const duplicate = this.db.prepare(`SELECT 1 FROM agent_host_accepts
|
|
273
|
+
WHERE delivery_id = ? AND attempt_id = ?`).get(request.delivery_id, request.attempt_id);
|
|
274
|
+
return { duplicate: Boolean(duplicate) };
|
|
275
|
+
}
|
|
276
|
+
if (this.externalConnections.get(request.connection_id) !== socket
|
|
277
|
+
|| pending.connection.connection_id !== request.connection_id
|
|
278
|
+
|| pending.connection.generation !== request.generation
|
|
279
|
+
|| pending.delivery_id !== request.delivery_id) {
|
|
280
|
+
throw new AgentRouterError('outcome_scope_mismatch', 'Host outcome does not match its delivery attempt.');
|
|
281
|
+
}
|
|
282
|
+
clearTimeout(pending.timer);
|
|
283
|
+
this.pendingExternal.delete(request.attempt_id);
|
|
284
|
+
pending.resolve(request.type === 'host_accept'
|
|
285
|
+
? { accepted: true, receipt: validateReceipt(request.receipt) }
|
|
286
|
+
: { accepted: false, receipt: { failure_code: request.failure_code } });
|
|
287
|
+
return { correlated: true };
|
|
288
|
+
}
|
|
289
|
+
registerConnection(registration) {
|
|
290
|
+
return this.db.transaction(() => {
|
|
291
|
+
const existingPrincipal = this.db.prepare(`
|
|
292
|
+
SELECT activation_event_sequence FROM agent_principals
|
|
293
|
+
WHERE project = ? AND principal_id = ?
|
|
294
|
+
`).get(registration.project, registration.principal_id);
|
|
295
|
+
if (!existingPrincipal) {
|
|
296
|
+
const checkpoint = this.db.prepare(`
|
|
297
|
+
SELECT COALESCE(MAX(event_sequence), 0) AS sequence
|
|
298
|
+
FROM agent_message_events WHERE project = ?
|
|
299
|
+
`).get(registration.project).sequence;
|
|
300
|
+
this.db.prepare(`
|
|
301
|
+
INSERT INTO agent_principals (project, principal_id, activation_event_sequence)
|
|
302
|
+
VALUES (?, ?, ?)
|
|
303
|
+
`).run(registration.project, registration.principal_id, checkpoint);
|
|
304
|
+
}
|
|
305
|
+
const session = this.db.prepare(`
|
|
306
|
+
SELECT principal_id, adapter_kind, last_generation
|
|
307
|
+
FROM agent_session_instances WHERE project = ? AND session_instance_id = ?
|
|
308
|
+
`).get(registration.project, registration.session_instance_id);
|
|
309
|
+
if (session && (session.principal_id !== registration.principal_id || session.adapter_kind !== registration.adapter_kind)) {
|
|
310
|
+
throw new AgentRouterError('session_identity_conflict', 'The session instance is already bound to another principal or adapter.');
|
|
311
|
+
}
|
|
312
|
+
if (!session) {
|
|
313
|
+
this.db.prepare(`
|
|
314
|
+
INSERT INTO agent_session_instances (
|
|
315
|
+
project, session_instance_id, principal_id, adapter_kind, last_generation
|
|
316
|
+
) VALUES (?, ?, ?, ?, 0)
|
|
317
|
+
`).run(registration.project, registration.session_instance_id, registration.principal_id, registration.adapter_kind);
|
|
318
|
+
}
|
|
319
|
+
const active = this.db.prepare(`
|
|
320
|
+
SELECT connection_id, project, principal_id, session_instance_id, generation,
|
|
321
|
+
adapter_kind, router_instance_id, lease_expires_at_ms
|
|
322
|
+
FROM agent_session_connections
|
|
323
|
+
WHERE project = ? AND session_instance_id = ? AND disconnected_at IS NULL
|
|
324
|
+
`).all(registration.project, registration.session_instance_id);
|
|
325
|
+
for (const previous of active) {
|
|
326
|
+
const previousSocket = this.externalConnections.get(previous.connection_id);
|
|
327
|
+
this.externalConnections.delete(previous.connection_id);
|
|
328
|
+
if (previousSocket && !previousSocket.destroyed) {
|
|
329
|
+
previousSocket.end(`${JSON.stringify({
|
|
330
|
+
version: 1,
|
|
331
|
+
type: 'session_superseded',
|
|
332
|
+
connection_id: previous.connection_id,
|
|
333
|
+
generation: previous.generation,
|
|
334
|
+
})}\n`);
|
|
335
|
+
}
|
|
336
|
+
for (const [attemptId, pending] of this.pendingExternal) {
|
|
337
|
+
if (pending.connection.connection_id !== previous.connection_id)
|
|
338
|
+
continue;
|
|
339
|
+
clearTimeout(pending.timer);
|
|
340
|
+
this.pendingExternal.delete(attemptId);
|
|
341
|
+
pending.resolve({ accepted: false, receipt: { failure_code: 'stale_generation' } });
|
|
342
|
+
}
|
|
343
|
+
this.db.prepare(`
|
|
344
|
+
UPDATE agent_session_connections
|
|
345
|
+
SET disconnected_at = CURRENT_TIMESTAMP, disconnect_reason = 'superseded'
|
|
346
|
+
WHERE connection_id = ?
|
|
347
|
+
`).run(previous.connection_id);
|
|
348
|
+
this.insertPresenceFact(previous, 'superseded', { replacement_router: this.router_instance_id });
|
|
349
|
+
}
|
|
350
|
+
const generation = (session?.last_generation ?? 0) + 1;
|
|
351
|
+
this.db.prepare(`
|
|
352
|
+
UPDATE agent_session_instances SET last_generation = ?
|
|
353
|
+
WHERE project = ? AND session_instance_id = ?
|
|
354
|
+
`).run(generation, registration.project, registration.session_instance_id);
|
|
355
|
+
const connection = {
|
|
356
|
+
connection_id: randomUUID(),
|
|
357
|
+
project: registration.project,
|
|
358
|
+
principal_id: registration.principal_id,
|
|
359
|
+
session_instance_id: registration.session_instance_id,
|
|
360
|
+
generation,
|
|
361
|
+
adapter_kind: registration.adapter_kind,
|
|
362
|
+
router_instance_id: this.router_instance_id,
|
|
363
|
+
lease_expires_at_ms: Date.now() + this.limits.lease_ms,
|
|
364
|
+
};
|
|
365
|
+
this.db.prepare(`
|
|
366
|
+
INSERT INTO agent_session_connections (
|
|
367
|
+
connection_id, project, principal_id, session_instance_id, generation,
|
|
368
|
+
adapter_kind, router_instance_id, lease_expires_at_ms
|
|
369
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
370
|
+
`).run(connection.connection_id, connection.project, connection.principal_id, connection.session_instance_id, connection.generation, connection.adapter_kind, connection.router_instance_id, connection.lease_expires_at_ms);
|
|
371
|
+
this.insertPresenceFact(connection, 'connected', {});
|
|
372
|
+
return connection;
|
|
373
|
+
}).immediate();
|
|
374
|
+
}
|
|
375
|
+
requireCurrentConnection(request) {
|
|
376
|
+
const row = this.db.prepare(`
|
|
377
|
+
SELECT connection_id, project, principal_id, session_instance_id, generation,
|
|
378
|
+
adapter_kind, router_instance_id, lease_expires_at_ms
|
|
379
|
+
FROM agent_session_connections
|
|
380
|
+
WHERE project = ? AND session_instance_id = ? AND connection_id = ? AND generation = ?
|
|
381
|
+
AND router_instance_id = ? AND disconnected_at IS NULL AND lease_expires_at_ms > ?
|
|
382
|
+
`).get(request.project, request.session_instance_id, request.connection_id, request.generation, this.router_instance_id, Date.now());
|
|
383
|
+
if (!row)
|
|
384
|
+
throw new AgentRouterStaleGenerationError();
|
|
385
|
+
return row;
|
|
386
|
+
}
|
|
387
|
+
insertPresenceFact(connection, kind, detail) {
|
|
388
|
+
this.db.prepare(`
|
|
389
|
+
INSERT INTO agent_presence_facts (
|
|
390
|
+
presence_fact_id, project, principal_id, session_instance_id,
|
|
391
|
+
connection_id, generation, presence_kind, detail_json
|
|
392
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
393
|
+
`).run(randomUUID(), connection.project, connection.principal_id, connection.session_instance_id, connection.connection_id, connection.generation, kind, JSON.stringify(detail));
|
|
394
|
+
}
|
|
395
|
+
async drainConnection(connection, hops) {
|
|
396
|
+
if (hops > this.limits.max_hops)
|
|
397
|
+
throw new AgentRouterError('hop_limit', 'Router hop limit exceeded.');
|
|
398
|
+
const checkpoint = this.db.prepare(`
|
|
399
|
+
SELECT activation_event_sequence FROM agent_principals
|
|
400
|
+
WHERE project = ? AND principal_id = ?
|
|
401
|
+
`).get(connection.project, connection.principal_id);
|
|
402
|
+
const upperBound = this.db.prepare(`
|
|
403
|
+
SELECT COALESCE(MAX(event_sequence), 0) AS event_sequence
|
|
404
|
+
FROM agent_message_events WHERE project = ?
|
|
405
|
+
`).get(connection.project).event_sequence;
|
|
406
|
+
const selectBatch = this.db.prepare(`
|
|
407
|
+
SELECT d.delivery_id, e.event_sequence
|
|
408
|
+
FROM agent_message_deliveries d
|
|
409
|
+
JOIN agent_message_events e ON e.delivery_id = d.delivery_id
|
|
410
|
+
LEFT JOIN agent_host_accepts h ON h.delivery_id = d.delivery_id
|
|
411
|
+
WHERE d.project = ? AND h.delivery_id IS NULL AND e.event_sequence <= ?
|
|
412
|
+
AND (e.event_sequence > ? OR (e.event_sequence = ? AND d.delivery_id > ?))
|
|
413
|
+
AND (
|
|
414
|
+
(d.target_kind = 'principal' AND d.recipient = ? AND e.event_sequence > ?)
|
|
415
|
+
OR (d.target_kind = 'session' AND d.recipient = ?)
|
|
416
|
+
)
|
|
417
|
+
ORDER BY e.event_sequence ASC, d.delivery_id ASC
|
|
418
|
+
LIMIT ?
|
|
419
|
+
`);
|
|
420
|
+
let drained = 0;
|
|
421
|
+
let cursorSequence = 0;
|
|
422
|
+
let cursorDeliveryId = '';
|
|
423
|
+
for (;;) {
|
|
424
|
+
const rows = selectBatch.all(connection.project, upperBound, cursorSequence, cursorSequence, cursorDeliveryId, connection.principal_id, checkpoint.activation_event_sequence, connection.session_instance_id, this.limits.drain_limit);
|
|
425
|
+
if (rows.length === 0)
|
|
426
|
+
break;
|
|
427
|
+
for (const row of rows) {
|
|
428
|
+
if (await this.dispatchDelivery(row.delivery_id, connection.project, hops, connection))
|
|
429
|
+
drained += 1;
|
|
430
|
+
}
|
|
431
|
+
const last = rows.at(-1);
|
|
432
|
+
cursorSequence = last.event_sequence;
|
|
433
|
+
cursorDeliveryId = last.delivery_id;
|
|
434
|
+
}
|
|
435
|
+
return drained;
|
|
436
|
+
}
|
|
437
|
+
async dispatchDelivery(deliveryId, project, hops, preferred) {
|
|
438
|
+
const inFlight = this.inFlightDeliveries.get(deliveryId);
|
|
439
|
+
if (inFlight)
|
|
440
|
+
return await inFlight.operation;
|
|
441
|
+
const entry = { operation: this.dispatchDeliveryOnce(deliveryId, project, hops, preferred) };
|
|
442
|
+
this.inFlightDeliveries.set(deliveryId, entry);
|
|
443
|
+
try {
|
|
444
|
+
return await entry.operation;
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
if (this.inFlightDeliveries.get(deliveryId) === entry) {
|
|
448
|
+
this.inFlightDeliveries.delete(deliveryId);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async dispatchDeliveryOnce(deliveryId, project, hops, preferred) {
|
|
453
|
+
if (hops > this.limits.max_hops)
|
|
454
|
+
throw new AgentRouterError('hop_limit', 'Router hop limit exceeded.');
|
|
455
|
+
if (this.db.prepare('SELECT 1 FROM agent_host_accepts WHERE delivery_id = ?').get(deliveryId))
|
|
456
|
+
return false;
|
|
457
|
+
const delivery = this.loadDelivery(deliveryId, project);
|
|
458
|
+
const connection = this.resolveConnection(delivery, preferred);
|
|
459
|
+
if (!connection)
|
|
460
|
+
return false;
|
|
461
|
+
const adapter = this.adapters.get(connection.adapter_kind);
|
|
462
|
+
const externalSocket = this.externalConnections.get(connection.connection_id);
|
|
463
|
+
const metadataDispatch = adapter?.dispatch_metadata_only;
|
|
464
|
+
const metadataOnly = Boolean(metadataDispatch);
|
|
465
|
+
if (metadataOnly && !externalSocket)
|
|
466
|
+
return false;
|
|
467
|
+
if (!metadataOnly && !externalSocket && !adapter?.dispatch)
|
|
468
|
+
return false;
|
|
469
|
+
const attempt = this.beginDispatchAttempt(delivery, connection);
|
|
470
|
+
let result;
|
|
471
|
+
try {
|
|
472
|
+
const common = {
|
|
473
|
+
dispatch_id: delivery.delivery_id,
|
|
474
|
+
attempt_id: attempt.attempt_id,
|
|
475
|
+
project: delivery.project,
|
|
476
|
+
principal_id: connection.principal_id,
|
|
477
|
+
session_instance_id: connection.session_instance_id,
|
|
478
|
+
connection_id: connection.connection_id,
|
|
479
|
+
generation: connection.generation,
|
|
480
|
+
hops,
|
|
481
|
+
};
|
|
482
|
+
if (metadataDispatch) {
|
|
483
|
+
result = await metadataDispatch({
|
|
484
|
+
...common,
|
|
485
|
+
routing: {
|
|
486
|
+
project: delivery.project,
|
|
487
|
+
recipient: delivery.recipient,
|
|
488
|
+
target_kind: parseTargetKind(delivery.target_kind),
|
|
489
|
+
message_id: delivery.message_id,
|
|
490
|
+
delivery_id: delivery.delivery_id,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
const envelope = fetchAgentMessage(this.db, {
|
|
496
|
+
project: delivery.project,
|
|
497
|
+
recipient: delivery.recipient,
|
|
498
|
+
target_kind: parseTargetKind(delivery.target_kind),
|
|
499
|
+
message_id: delivery.message_id,
|
|
500
|
+
});
|
|
501
|
+
const input = {
|
|
502
|
+
...common,
|
|
503
|
+
untrusted_payload: true,
|
|
504
|
+
envelope,
|
|
505
|
+
};
|
|
506
|
+
result = externalSocket
|
|
507
|
+
? await this.dispatchExternal(externalSocket, connection, attempt.attempt_id, input)
|
|
508
|
+
: await adapter.dispatch(input);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
this.finishAttempt(attempt.attempt_id, 'adapter_failed', 'adapter_error');
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
if (!result.accepted) {
|
|
516
|
+
this.finishAttempt(attempt.attempt_id, 'adapter_rejected', adapterFailureCode(result.receipt));
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
const receipt = validateReceipt(result.receipt ?? {});
|
|
520
|
+
try {
|
|
521
|
+
this.requireCurrentConnection(connection);
|
|
522
|
+
if (metadataOnly && (!externalSocket
|
|
523
|
+
|| externalSocket.destroyed
|
|
524
|
+
|| this.externalConnections.get(connection.connection_id) !== externalSocket))
|
|
525
|
+
throw new AgentRouterStaleGenerationError();
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
this.finishAttempt(attempt.attempt_id, 'stale_generation', 'stale_generation');
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return this.db.transaction(() => {
|
|
532
|
+
const existing = this.db.prepare(`
|
|
533
|
+
SELECT 1 FROM agent_host_accepts WHERE delivery_id = ?
|
|
534
|
+
`).get(delivery.delivery_id);
|
|
535
|
+
if (existing)
|
|
536
|
+
return false;
|
|
537
|
+
this.db.prepare(`
|
|
538
|
+
INSERT INTO agent_host_accepts (
|
|
539
|
+
host_accept_id, attempt_id, delivery_id, adapter_kind, receipt_json
|
|
540
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
541
|
+
`).run(randomUUID(), attempt.attempt_id, delivery.delivery_id, connection.adapter_kind, JSON.stringify(receipt));
|
|
542
|
+
this.finishAttempt(attempt.attempt_id, 'adapter_returned', null);
|
|
543
|
+
return true;
|
|
544
|
+
}).immediate();
|
|
545
|
+
}
|
|
546
|
+
dispatchExternal(socket, connection, attemptId, input) {
|
|
547
|
+
const frame = Buffer.from(`${JSON.stringify({
|
|
548
|
+
version: 1,
|
|
549
|
+
type: 'deliver',
|
|
550
|
+
request_id: randomUUID(),
|
|
551
|
+
attempt_id: attemptId,
|
|
552
|
+
delivery_id: input.dispatch_id,
|
|
553
|
+
project: input.project,
|
|
554
|
+
principal_id: input.principal_id,
|
|
555
|
+
session_instance_id: input.session_instance_id,
|
|
556
|
+
connection_id: input.connection_id,
|
|
557
|
+
generation: input.generation,
|
|
558
|
+
hops: input.hops,
|
|
559
|
+
untrusted_payload: true,
|
|
560
|
+
envelope: input.envelope,
|
|
561
|
+
})}\n`, 'utf8');
|
|
562
|
+
if (frame.length > this.limits.max_frame_bytes) {
|
|
563
|
+
return Promise.resolve({ accepted: false, receipt: { failure_code: 'frame_too_large' } });
|
|
564
|
+
}
|
|
565
|
+
return new Promise((resolve) => {
|
|
566
|
+
const timer = setTimeout(() => {
|
|
567
|
+
this.pendingExternal.delete(attemptId);
|
|
568
|
+
resolve({ accepted: false, receipt: { failure_code: 'host_outcome_timeout' } });
|
|
569
|
+
}, this.limits.delivery_timeout_ms);
|
|
570
|
+
timer.unref();
|
|
571
|
+
this.pendingExternal.set(attemptId, {
|
|
572
|
+
connection,
|
|
573
|
+
delivery_id: input.dispatch_id,
|
|
574
|
+
resolve,
|
|
575
|
+
timer,
|
|
576
|
+
});
|
|
577
|
+
if (socket.destroyed) {
|
|
578
|
+
clearTimeout(timer);
|
|
579
|
+
this.pendingExternal.delete(attemptId);
|
|
580
|
+
resolve({ accepted: false, receipt: { failure_code: 'adapter_disconnected' } });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
socket.write(frame);
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
loadDelivery(deliveryId, project) {
|
|
587
|
+
const row = this.db.prepare(`
|
|
588
|
+
SELECT d.delivery_id, d.message_id, d.project, d.recipient, d.target_kind, e.event_sequence
|
|
589
|
+
FROM agent_message_deliveries d
|
|
590
|
+
JOIN agent_message_events e ON e.delivery_id = d.delivery_id
|
|
591
|
+
WHERE d.delivery_id = ? AND d.project = ?
|
|
592
|
+
`).get(validateField('delivery_id', deliveryId), validateField('project', project));
|
|
593
|
+
if (!row)
|
|
594
|
+
throw new AgentRouterError('delivery_not_found', 'The delivery does not exist in this project.');
|
|
595
|
+
parseTargetKind(row.target_kind);
|
|
596
|
+
return row;
|
|
597
|
+
}
|
|
598
|
+
resolveConnection(delivery, preferred) {
|
|
599
|
+
const targetKind = parseTargetKind(delivery.target_kind);
|
|
600
|
+
if (preferred && this.connectionMatchesDelivery(preferred, delivery, targetKind)) {
|
|
601
|
+
try {
|
|
602
|
+
return this.requireCurrentConnection(preferred);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
return undefined;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (targetKind === 'principal') {
|
|
609
|
+
const principal = this.db.prepare(`
|
|
610
|
+
SELECT activation_event_sequence FROM agent_principals
|
|
611
|
+
WHERE project = ? AND principal_id = ?
|
|
612
|
+
`).get(delivery.project, delivery.recipient);
|
|
613
|
+
if (!principal || delivery.event_sequence <= principal.activation_event_sequence)
|
|
614
|
+
return undefined;
|
|
615
|
+
return this.db.prepare(`
|
|
616
|
+
SELECT connection_id, project, principal_id, session_instance_id, generation,
|
|
617
|
+
adapter_kind, router_instance_id, lease_expires_at_ms
|
|
618
|
+
FROM agent_session_connections
|
|
619
|
+
WHERE project = ? AND principal_id = ? AND router_instance_id = ?
|
|
620
|
+
AND disconnected_at IS NULL AND lease_expires_at_ms > ?
|
|
621
|
+
ORDER BY connected_at DESC, rowid DESC
|
|
622
|
+
LIMIT 1
|
|
623
|
+
`).get(delivery.project, delivery.recipient, this.router_instance_id, Date.now());
|
|
624
|
+
}
|
|
625
|
+
return this.db.prepare(`
|
|
626
|
+
SELECT connection_id, project, principal_id, session_instance_id, generation,
|
|
627
|
+
adapter_kind, router_instance_id, lease_expires_at_ms
|
|
628
|
+
FROM agent_session_connections
|
|
629
|
+
WHERE project = ? AND session_instance_id = ? AND router_instance_id = ?
|
|
630
|
+
AND disconnected_at IS NULL AND lease_expires_at_ms > ?
|
|
631
|
+
ORDER BY generation DESC
|
|
632
|
+
LIMIT 1
|
|
633
|
+
`).get(delivery.project, delivery.recipient, this.router_instance_id, Date.now());
|
|
634
|
+
}
|
|
635
|
+
connectionMatchesDelivery(connection, delivery, targetKind) {
|
|
636
|
+
if (connection.project !== delivery.project)
|
|
637
|
+
return false;
|
|
638
|
+
return targetKind === 'principal'
|
|
639
|
+
? connection.principal_id === delivery.recipient
|
|
640
|
+
: connection.session_instance_id === delivery.recipient;
|
|
641
|
+
}
|
|
642
|
+
beginDispatchAttempt(delivery, connection) {
|
|
643
|
+
return this.db.transaction(() => {
|
|
644
|
+
this.requireCurrentConnection(connection);
|
|
645
|
+
if (this.db.prepare('SELECT 1 FROM agent_host_accepts WHERE delivery_id = ?').get(delivery.delivery_id)) {
|
|
646
|
+
throw new AgentRouterError('already_accepted', 'The delivery already has a host acceptance.');
|
|
647
|
+
}
|
|
648
|
+
const attemptNumber = this.db.prepare(`
|
|
649
|
+
SELECT COALESCE(MAX(attempt_number), 0) + 1 AS next
|
|
650
|
+
FROM agent_dispatch_attempts WHERE delivery_id = ?
|
|
651
|
+
`).get(delivery.delivery_id).next;
|
|
652
|
+
const attemptId = randomUUID();
|
|
653
|
+
this.db.prepare(`
|
|
654
|
+
INSERT INTO agent_dispatch_attempts (
|
|
655
|
+
attempt_id, delivery_id, project, principal_id, session_instance_id,
|
|
656
|
+
connection_id, generation, router_instance_id, attempt_number
|
|
657
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
658
|
+
`).run(attemptId, delivery.delivery_id, delivery.project, connection.principal_id, connection.session_instance_id, connection.connection_id, connection.generation, connection.router_instance_id, attemptNumber);
|
|
659
|
+
return { attempt_id: attemptId, attempt_number: attemptNumber };
|
|
660
|
+
}).immediate();
|
|
661
|
+
}
|
|
662
|
+
finishAttempt(attemptId, result, failureCode) {
|
|
663
|
+
this.db.prepare(`
|
|
664
|
+
UPDATE agent_dispatch_attempts
|
|
665
|
+
SET result = ?, failure_code = ?, completed_at = CURRENT_TIMESTAMP
|
|
666
|
+
WHERE attempt_id = ?
|
|
667
|
+
`).run(result, failureCode, attemptId);
|
|
668
|
+
}
|
|
669
|
+
writeError(socket, requestId, code, message) {
|
|
670
|
+
this.writeResponse(socket, {
|
|
671
|
+
version: 1,
|
|
672
|
+
request_id: requestId,
|
|
673
|
+
ok: false,
|
|
674
|
+
error: { code, message },
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
writeResponse(socket, response) {
|
|
678
|
+
if (!socket.destroyed)
|
|
679
|
+
socket.write(`${JSON.stringify(response)}\n`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
export function createAgentRouterNotifier(socketPath) {
|
|
683
|
+
const validatedPath = validateSocketPath(socketPath);
|
|
684
|
+
return {
|
|
685
|
+
async notify(hint) {
|
|
686
|
+
await sendAgentRouterRequest(validatedPath, {
|
|
687
|
+
version: 1,
|
|
688
|
+
type: 'notify',
|
|
689
|
+
request_id: randomUUID(),
|
|
690
|
+
project: hint.project,
|
|
691
|
+
delivery_id: hint.delivery_id,
|
|
692
|
+
hops: 0,
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
export async function sendAgentRouterRequest(socketPath, request, timeoutMs = DEFAULT_CLIENT_TIMEOUT_MS) {
|
|
698
|
+
const frame = Buffer.from(`${JSON.stringify(request)}\n`, 'utf8');
|
|
699
|
+
if (frame.length > AGENT_ROUTER_MAX_FRAME_BYTES) {
|
|
700
|
+
throw new AgentRouterProtocolError('frame_too_large', 'Router request exceeds the byte limit.');
|
|
701
|
+
}
|
|
702
|
+
return new Promise((resolve, reject) => {
|
|
703
|
+
const socket = net.createConnection(validateSocketPath(socketPath));
|
|
704
|
+
let pending = Buffer.alloc(0);
|
|
705
|
+
let settled = false;
|
|
706
|
+
const timer = setTimeout(() => finish(new AgentRouterProtocolError('timeout', 'Router request timed out.')), timeoutMs);
|
|
707
|
+
const finish = (error, value) => {
|
|
708
|
+
if (settled)
|
|
709
|
+
return;
|
|
710
|
+
settled = true;
|
|
711
|
+
clearTimeout(timer);
|
|
712
|
+
socket.destroy();
|
|
713
|
+
if (error) {
|
|
714
|
+
reject(error);
|
|
715
|
+
}
|
|
716
|
+
else if (value === undefined) {
|
|
717
|
+
reject(new AgentRouterProtocolError('invalid_response', 'Router response omitted its result object.'));
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
resolve(value);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
socket.on('connect', () => socket.write(frame));
|
|
724
|
+
socket.on('data', (chunk) => {
|
|
725
|
+
pending = Buffer.concat([pending, chunk]);
|
|
726
|
+
if (pending.length > AGENT_ROUTER_MAX_FRAME_BYTES) {
|
|
727
|
+
finish(new AgentRouterProtocolError('frame_too_large', 'Router response exceeds the byte limit.'));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const newline = pending.indexOf(0x0a);
|
|
731
|
+
if (newline < 0)
|
|
732
|
+
return;
|
|
733
|
+
try {
|
|
734
|
+
const response = JSON.parse(pending.subarray(0, newline).toString('utf8'));
|
|
735
|
+
if (response.version !== 1 || response.request_id !== request.request_id) {
|
|
736
|
+
throw new AgentRouterProtocolError('invalid_response', 'Router response identity does not match.');
|
|
737
|
+
}
|
|
738
|
+
if (!response.ok)
|
|
739
|
+
throw new AgentRouterProtocolError(response.error.code, response.error.message);
|
|
740
|
+
finish(undefined, validateRouterSuccessResult(request, response.result));
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
socket.on('error', (error) => finish(error));
|
|
747
|
+
socket.on('end', () => {
|
|
748
|
+
if (!settled)
|
|
749
|
+
finish(new AgentRouterProtocolError('connection_closed', 'Router closed without a response.'));
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function validateRouterSuccessResult(request, value) {
|
|
754
|
+
if (!isPlainObject(value)) {
|
|
755
|
+
throw new AgentRouterProtocolError('invalid_response', 'Router response result must be an object.');
|
|
756
|
+
}
|
|
757
|
+
const requireResultString = (field) => {
|
|
758
|
+
const candidate = value[field];
|
|
759
|
+
if (typeof candidate !== 'string' || candidate.length === 0 || candidate.length > MAX_FIELD_LENGTH) {
|
|
760
|
+
throw new AgentRouterProtocolError('invalid_response', `Router response omitted a valid ${field}.`);
|
|
761
|
+
}
|
|
762
|
+
return candidate;
|
|
763
|
+
};
|
|
764
|
+
const requireResultInteger = (field, minimum = 1) => {
|
|
765
|
+
const candidate = value[field];
|
|
766
|
+
if (!Number.isSafeInteger(candidate) || candidate < minimum) {
|
|
767
|
+
throw new AgentRouterProtocolError('invalid_response', `Router response omitted a valid ${field}.`);
|
|
768
|
+
}
|
|
769
|
+
return candidate;
|
|
770
|
+
};
|
|
771
|
+
const requireResultBoolean = (field) => {
|
|
772
|
+
const candidate = value[field];
|
|
773
|
+
if (typeof candidate !== 'boolean') {
|
|
774
|
+
throw new AgentRouterProtocolError('invalid_response', `Router response omitted a valid ${field}.`);
|
|
775
|
+
}
|
|
776
|
+
return candidate;
|
|
777
|
+
};
|
|
778
|
+
switch (request.type) {
|
|
779
|
+
case 'register':
|
|
780
|
+
requireResultString('connection_id');
|
|
781
|
+
requireResultInteger('generation');
|
|
782
|
+
requireResultInteger('lease_ms');
|
|
783
|
+
requireResultBoolean('drain_scheduled');
|
|
784
|
+
break;
|
|
785
|
+
case 'notify':
|
|
786
|
+
requireResultBoolean('delivered');
|
|
787
|
+
break;
|
|
788
|
+
case 'heartbeat':
|
|
789
|
+
requireResultInteger('generation');
|
|
790
|
+
requireResultInteger('lease_ms');
|
|
791
|
+
break;
|
|
792
|
+
case 'disconnect':
|
|
793
|
+
if (value.disconnected !== true) {
|
|
794
|
+
throw new AgentRouterProtocolError('invalid_response', 'Router response did not confirm disconnect.');
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
case 'host_accept':
|
|
798
|
+
case 'host_reject':
|
|
799
|
+
if (value.correlated !== true && typeof value.duplicate !== 'boolean') {
|
|
800
|
+
throw new AgentRouterProtocolError('invalid_response', 'Router response omitted its host-outcome correlation.');
|
|
801
|
+
}
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
return value;
|
|
805
|
+
}
|
|
806
|
+
function parseRequest(frame, maxHops) {
|
|
807
|
+
let value;
|
|
808
|
+
try {
|
|
809
|
+
value = JSON.parse(frame.toString('utf8'));
|
|
810
|
+
}
|
|
811
|
+
catch {
|
|
812
|
+
throw new AgentRouterProtocolError('invalid_json', 'Router frames must contain one JSON object.');
|
|
813
|
+
}
|
|
814
|
+
if (!isPlainObject(value))
|
|
815
|
+
throw new AgentRouterProtocolError('invalid_frame', 'Router frame must be an object.');
|
|
816
|
+
if (value.version !== 1)
|
|
817
|
+
throw new AgentRouterProtocolError('unsupported_version', 'Unsupported router protocol version.');
|
|
818
|
+
const type = value.type;
|
|
819
|
+
const common = ['version', 'type', 'request_id', 'hops'];
|
|
820
|
+
const requestId = validateField('request_id', value.request_id);
|
|
821
|
+
const hops = validateInteger('hops', value.hops, 0, maxHops);
|
|
822
|
+
switch (type) {
|
|
823
|
+
case 'register':
|
|
824
|
+
assertAllowedKeys(value, [
|
|
825
|
+
...common, 'project', 'principal_id', 'session_instance_id', 'adapter_kind', 'auth_token',
|
|
826
|
+
]);
|
|
827
|
+
return {
|
|
828
|
+
version: 1,
|
|
829
|
+
type,
|
|
830
|
+
request_id: requestId,
|
|
831
|
+
project: validateField('project', value.project),
|
|
832
|
+
principal_id: validateField('principal_id', value.principal_id),
|
|
833
|
+
session_instance_id: validateField('session_instance_id', value.session_instance_id),
|
|
834
|
+
adapter_kind: validateField('adapter_kind', value.adapter_kind),
|
|
835
|
+
...(value.auth_token === undefined ? {} : { auth_token: validateField('auth_token', value.auth_token) }),
|
|
836
|
+
hops,
|
|
837
|
+
};
|
|
838
|
+
case 'notify':
|
|
839
|
+
assertAllowedKeys(value, [...common, 'project', 'delivery_id']);
|
|
840
|
+
return {
|
|
841
|
+
version: 1,
|
|
842
|
+
type,
|
|
843
|
+
request_id: requestId,
|
|
844
|
+
project: validateField('project', value.project),
|
|
845
|
+
delivery_id: validateField('delivery_id', value.delivery_id),
|
|
846
|
+
hops,
|
|
847
|
+
};
|
|
848
|
+
case 'heartbeat':
|
|
849
|
+
case 'disconnect':
|
|
850
|
+
assertAllowedKeys(value, [
|
|
851
|
+
...common, 'project', 'session_instance_id', 'connection_id', 'generation',
|
|
852
|
+
]);
|
|
853
|
+
return {
|
|
854
|
+
version: 1,
|
|
855
|
+
type,
|
|
856
|
+
request_id: requestId,
|
|
857
|
+
project: validateField('project', value.project),
|
|
858
|
+
session_instance_id: validateField('session_instance_id', value.session_instance_id),
|
|
859
|
+
connection_id: validateField('connection_id', value.connection_id),
|
|
860
|
+
generation: validateInteger('generation', value.generation, 1, Number.MAX_SAFE_INTEGER),
|
|
861
|
+
hops,
|
|
862
|
+
};
|
|
863
|
+
case 'host_accept':
|
|
864
|
+
assertAllowedKeys(value, [
|
|
865
|
+
...common, 'attempt_id', 'delivery_id', 'connection_id', 'generation', 'receipt',
|
|
866
|
+
]);
|
|
867
|
+
if (!isPlainObject(value.receipt)) {
|
|
868
|
+
throw new AgentRouterProtocolError('invalid_field', 'receipt must be an object.');
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
version: 1,
|
|
872
|
+
type,
|
|
873
|
+
request_id: requestId,
|
|
874
|
+
attempt_id: validateField('attempt_id', value.attempt_id),
|
|
875
|
+
delivery_id: validateField('delivery_id', value.delivery_id),
|
|
876
|
+
connection_id: validateField('connection_id', value.connection_id),
|
|
877
|
+
generation: validateInteger('generation', value.generation, 1, Number.MAX_SAFE_INTEGER),
|
|
878
|
+
receipt: validateReceipt(value.receipt),
|
|
879
|
+
hops,
|
|
880
|
+
};
|
|
881
|
+
case 'host_reject':
|
|
882
|
+
assertAllowedKeys(value, [
|
|
883
|
+
...common, 'attempt_id', 'delivery_id', 'connection_id', 'generation', 'failure_code',
|
|
884
|
+
]);
|
|
885
|
+
return {
|
|
886
|
+
version: 1,
|
|
887
|
+
type,
|
|
888
|
+
request_id: requestId,
|
|
889
|
+
attempt_id: validateField('attempt_id', value.attempt_id),
|
|
890
|
+
delivery_id: validateField('delivery_id', value.delivery_id),
|
|
891
|
+
connection_id: validateField('connection_id', value.connection_id),
|
|
892
|
+
generation: validateInteger('generation', value.generation, 1, Number.MAX_SAFE_INTEGER),
|
|
893
|
+
failure_code: validateField('failure_code', value.failure_code),
|
|
894
|
+
hops,
|
|
895
|
+
};
|
|
896
|
+
default:
|
|
897
|
+
throw new AgentRouterProtocolError('unsupported_type', 'Unsupported router frame type.');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function assertAllowedKeys(value, allowed) {
|
|
901
|
+
const allowedSet = new Set(allowed);
|
|
902
|
+
const unexpected = Object.keys(value).find((key) => !allowedSet.has(key));
|
|
903
|
+
if (unexpected) {
|
|
904
|
+
throw new AgentRouterProtocolError('unexpected_field', `Router frame contains unsupported field ${unexpected}.`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
function validateField(label, value) {
|
|
908
|
+
if (typeof value !== 'string')
|
|
909
|
+
throw new AgentRouterProtocolError('invalid_field', `${label} must be a string.`);
|
|
910
|
+
const normalized = value.trim();
|
|
911
|
+
if (!normalized || normalized.length > MAX_FIELD_LENGTH) {
|
|
912
|
+
throw new AgentRouterProtocolError('invalid_field', `${label} must contain 1-${MAX_FIELD_LENGTH} characters.`);
|
|
913
|
+
}
|
|
914
|
+
return normalized;
|
|
915
|
+
}
|
|
916
|
+
function validateInteger(label, value, min, max) {
|
|
917
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
918
|
+
throw new AgentRouterProtocolError('invalid_field', `${label} is outside the allowed integer range.`);
|
|
919
|
+
}
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
function listenOnSocket(server, socketPath) {
|
|
923
|
+
return new Promise((resolve, reject) => {
|
|
924
|
+
const onError = (error) => {
|
|
925
|
+
server.off('listening', onListening);
|
|
926
|
+
reject(error);
|
|
927
|
+
};
|
|
928
|
+
const onListening = () => {
|
|
929
|
+
server.off('error', onError);
|
|
930
|
+
resolve();
|
|
931
|
+
};
|
|
932
|
+
server.once('error', onError);
|
|
933
|
+
server.once('listening', onListening);
|
|
934
|
+
server.listen(socketPath);
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async function removeOrphanedSocket(socketPath) {
|
|
938
|
+
const initial = lstatIfPresent(socketPath);
|
|
939
|
+
if (!initial)
|
|
940
|
+
return true;
|
|
941
|
+
assertSafeStaleSocketCandidate(initial);
|
|
942
|
+
if (await probeSocket(socketPath) !== 'refused')
|
|
943
|
+
return false;
|
|
944
|
+
const afterProbe = lstatIfPresent(socketPath);
|
|
945
|
+
if (!afterProbe || !sameSocketIdentity(initial, afterProbe))
|
|
946
|
+
return true;
|
|
947
|
+
if (await probeSocket(socketPath) !== 'refused')
|
|
948
|
+
return false;
|
|
949
|
+
const beforeUnlink = lstatIfPresent(socketPath);
|
|
950
|
+
if (!beforeUnlink || !sameSocketIdentity(initial, beforeUnlink))
|
|
951
|
+
return true;
|
|
952
|
+
fs.unlinkSync(socketPath);
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
async function acquireStartupLock(socketPath) {
|
|
956
|
+
const lockPath = `${socketPath}.startup.lock`;
|
|
957
|
+
const deadline = Date.now() + STARTUP_LOCK_WAIT_MS;
|
|
958
|
+
for (;;) {
|
|
959
|
+
try {
|
|
960
|
+
const fd = fs.openSync(lockPath, 'wx', 0o600);
|
|
961
|
+
const stat = fs.fstatSync(fd);
|
|
962
|
+
const lock = { fd, path: lockPath, identity: { dev: stat.dev, ino: stat.ino } };
|
|
963
|
+
try {
|
|
964
|
+
fs.writeFileSync(fd, `${process.pid}\n`, 'utf8');
|
|
965
|
+
return lock;
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
fs.closeSync(fd);
|
|
969
|
+
unlinkPathIfSame(lockPath, lock.identity);
|
|
970
|
+
throw error;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
catch (error) {
|
|
974
|
+
if (error.code !== 'EEXIST')
|
|
975
|
+
throw error;
|
|
976
|
+
const existing = readPrivateLockIdentity(lockPath);
|
|
977
|
+
if (startupLockIsStale(lockPath, existing)) {
|
|
978
|
+
unlinkPathIfSame(lockPath, existing);
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
if (Date.now() >= deadline) {
|
|
982
|
+
throw new AgentRouterError('router_start_in_progress', 'Another router process is starting.');
|
|
983
|
+
}
|
|
984
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function readPrivateLockIdentity(lockPath) {
|
|
989
|
+
const stat = fs.lstatSync(lockPath);
|
|
990
|
+
if (!stat.isFile() || (stat.mode & 0o077) !== 0) {
|
|
991
|
+
throw new AgentRouterError('insecure_startup_lock', 'Router startup lock must be an owner-private regular file.');
|
|
992
|
+
}
|
|
993
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
994
|
+
throw new AgentRouterError('foreign_startup_lock', 'Router startup lock is owned by another user.');
|
|
995
|
+
}
|
|
996
|
+
return { dev: stat.dev, ino: stat.ino, mtimeMs: stat.mtimeMs };
|
|
997
|
+
}
|
|
998
|
+
function startupLockIsStale(lockPath, identity) {
|
|
999
|
+
if (Date.now() - identity.mtimeMs >= STARTUP_LOCK_STALE_MS)
|
|
1000
|
+
return true;
|
|
1001
|
+
try {
|
|
1002
|
+
const raw = fs.readFileSync(lockPath, 'utf8').trim();
|
|
1003
|
+
if (!/^[1-9][0-9]*$/.test(raw))
|
|
1004
|
+
return false;
|
|
1005
|
+
process.kill(Number(raw), 0);
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
return error.code === 'ESRCH';
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function releaseStartupLock(lock) {
|
|
1013
|
+
try {
|
|
1014
|
+
fs.closeSync(lock.fd);
|
|
1015
|
+
}
|
|
1016
|
+
finally {
|
|
1017
|
+
unlinkPathIfSame(lock.path, lock.identity);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function probeSocket(socketPath) {
|
|
1021
|
+
return new Promise((resolve) => {
|
|
1022
|
+
const socket = net.createConnection(socketPath);
|
|
1023
|
+
let settled = false;
|
|
1024
|
+
const timer = setTimeout(() => finish('indeterminate'), STALE_SOCKET_PROBE_TIMEOUT_MS);
|
|
1025
|
+
timer.unref();
|
|
1026
|
+
const finish = (result) => {
|
|
1027
|
+
if (settled)
|
|
1028
|
+
return;
|
|
1029
|
+
settled = true;
|
|
1030
|
+
clearTimeout(timer);
|
|
1031
|
+
socket.destroy();
|
|
1032
|
+
resolve(result);
|
|
1033
|
+
};
|
|
1034
|
+
socket.once('connect', () => finish('connected'));
|
|
1035
|
+
socket.once('error', (error) => {
|
|
1036
|
+
if (error.code === 'ECONNREFUSED')
|
|
1037
|
+
finish('refused');
|
|
1038
|
+
else if (error.code === 'ENOENT')
|
|
1039
|
+
finish('missing');
|
|
1040
|
+
else
|
|
1041
|
+
finish('indeterminate');
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
function assertSafeStaleSocketCandidate(stat) {
|
|
1046
|
+
if (!stat.isSocket()) {
|
|
1047
|
+
throw new AgentRouterError('socket_path_occupied', 'Router socket path is occupied by a non-socket file.');
|
|
1048
|
+
}
|
|
1049
|
+
if ((stat.mode & 0o077) !== 0) {
|
|
1050
|
+
throw new AgentRouterError('insecure_existing_socket', 'Existing router socket is not owner-private.');
|
|
1051
|
+
}
|
|
1052
|
+
if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
|
|
1053
|
+
throw new AgentRouterError('foreign_existing_socket', 'Existing router socket is owned by another user.');
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function lstatIfPresent(socketPath) {
|
|
1057
|
+
try {
|
|
1058
|
+
return fs.lstatSync(socketPath);
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
if (error.code === 'ENOENT')
|
|
1062
|
+
return undefined;
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function readSocketIdentity(socketPath) {
|
|
1067
|
+
const stat = fs.lstatSync(socketPath);
|
|
1068
|
+
if (!stat.isSocket())
|
|
1069
|
+
throw new AgentRouterError('invalid_socket', 'Router did not create a Unix socket.');
|
|
1070
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
1071
|
+
}
|
|
1072
|
+
function sameSocketIdentity(expected, actual) {
|
|
1073
|
+
return expected.dev === actual.dev && expected.ino === actual.ino;
|
|
1074
|
+
}
|
|
1075
|
+
function unlinkSocketIfSame(socketPath, expected) {
|
|
1076
|
+
const current = lstatIfPresent(socketPath);
|
|
1077
|
+
if (!current || !current.isSocket() || !sameSocketIdentity(expected, current))
|
|
1078
|
+
return;
|
|
1079
|
+
fs.unlinkSync(socketPath);
|
|
1080
|
+
}
|
|
1081
|
+
function unlinkPathIfSame(targetPath, expected) {
|
|
1082
|
+
const current = lstatIfPresent(targetPath);
|
|
1083
|
+
if (!current || !sameSocketIdentity(expected, current))
|
|
1084
|
+
return;
|
|
1085
|
+
fs.unlinkSync(targetPath);
|
|
1086
|
+
}
|
|
1087
|
+
function validateSocketPath(socketPath) {
|
|
1088
|
+
if (typeof socketPath !== 'string' || !path.isAbsolute(socketPath) || Buffer.byteLength(socketPath) > 103) {
|
|
1089
|
+
throw new AgentRouterProtocolError('invalid_socket_path', 'Router socket path must be absolute and at most 103 bytes.');
|
|
1090
|
+
}
|
|
1091
|
+
return socketPath;
|
|
1092
|
+
}
|
|
1093
|
+
function normalizeLimits(limits = {}) {
|
|
1094
|
+
return {
|
|
1095
|
+
max_frame_bytes: validateInteger('max_frame_bytes', limits.max_frame_bytes ?? AGENT_ROUTER_MAX_FRAME_BYTES, 256, AGENT_ROUTER_MAX_FRAME_BYTES),
|
|
1096
|
+
max_hops: validateInteger('max_hops', limits.max_hops ?? AGENT_ROUTER_MAX_HOPS, 1, 16),
|
|
1097
|
+
max_frames_per_window: validateInteger('max_frames_per_window', limits.max_frames_per_window ?? DEFAULT_RATE_LIMIT, 1, 10_000),
|
|
1098
|
+
rate_window_ms: validateInteger('rate_window_ms', limits.rate_window_ms ?? DEFAULT_RATE_WINDOW_MS, 10, 60 * 60_000),
|
|
1099
|
+
lease_ms: validateInteger('lease_ms', limits.lease_ms ?? DEFAULT_LEASE_MS, 100, MAX_LEASE_MS),
|
|
1100
|
+
drain_limit: validateInteger('drain_limit', limits.drain_limit ?? DEFAULT_DRAIN_LIMIT, 1, 10_000),
|
|
1101
|
+
delivery_timeout_ms: validateInteger('delivery_timeout_ms', limits.delivery_timeout_ms ?? 10_000, 100, 10 * 60_000),
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function validateReceipt(value) {
|
|
1105
|
+
if (!isPlainObject(value))
|
|
1106
|
+
throw new AgentRouterError('invalid_adapter_receipt', 'Adapter receipt must be an object.');
|
|
1107
|
+
let json;
|
|
1108
|
+
try {
|
|
1109
|
+
json = JSON.stringify(value);
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
throw new AgentRouterError('invalid_adapter_receipt', 'Adapter receipt must be JSON serializable.');
|
|
1113
|
+
}
|
|
1114
|
+
if (Buffer.byteLength(json, 'utf8') > MAX_ADAPTER_RECEIPT_BYTES) {
|
|
1115
|
+
throw new AgentRouterError('invalid_adapter_receipt', 'Adapter receipt exceeds the byte limit.');
|
|
1116
|
+
}
|
|
1117
|
+
return value;
|
|
1118
|
+
}
|
|
1119
|
+
function parseTargetKind(value) {
|
|
1120
|
+
if (value === 'principal' || value === 'session')
|
|
1121
|
+
return value;
|
|
1122
|
+
throw new AgentRouterError('invalid_target_kind', 'Stored delivery target kind is invalid.');
|
|
1123
|
+
}
|
|
1124
|
+
function adapterFailureCode(receipt) {
|
|
1125
|
+
const value = receipt?.failure_code;
|
|
1126
|
+
return typeof value === 'string' && /^[a-z0-9_]{1,64}$/.test(value)
|
|
1127
|
+
? value
|
|
1128
|
+
: 'adapter_rejected';
|
|
1129
|
+
}
|
|
1130
|
+
function isPlainObject(value) {
|
|
1131
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1132
|
+
}
|
|
1133
|
+
function toRouterError(error) {
|
|
1134
|
+
if (error instanceof AgentRouterError)
|
|
1135
|
+
return error;
|
|
1136
|
+
return new AgentRouterError('internal_error', 'The router could not process the request.');
|
|
1137
|
+
}
|
|
1138
|
+
//# sourceMappingURL=agent-router.js.map
|