@masons/agent-network 0.5.28 → 0.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/broker/broker-daemon.d.ts +34 -0
- package/dist/broker/broker-daemon.d.ts.map +1 -0
- package/dist/broker/broker-daemon.js +471 -0
- package/dist/broker/closed-endpoint-lookup.d.ts +25 -0
- package/dist/broker/closed-endpoint-lookup.d.ts.map +1 -0
- package/dist/broker/closed-endpoint-lookup.js +59 -0
- package/dist/broker/connector-ws.d.ts +47 -0
- package/dist/broker/connector-ws.d.ts.map +1 -0
- package/dist/broker/connector-ws.js +60 -0
- package/dist/broker/control-event-dispatcher.d.ts +20 -0
- package/dist/broker/control-event-dispatcher.d.ts.map +1 -0
- package/dist/broker/control-event-dispatcher.js +45 -0
- package/dist/broker/control-event-types.d.ts +30 -0
- package/dist/broker/control-event-types.d.ts.map +1 -0
- package/dist/broker/control-event-types.js +1 -0
- package/dist/broker/correlation-ring.d.ts +10 -0
- package/dist/broker/correlation-ring.d.ts.map +1 -0
- package/dist/broker/correlation-ring.js +32 -0
- package/dist/broker/discovery-file.d.ts +12 -0
- package/dist/broker/discovery-file.d.ts.map +1 -0
- package/dist/broker/discovery-file.js +77 -0
- package/dist/broker/endpoint-registry.d.ts +53 -0
- package/dist/broker/endpoint-registry.d.ts.map +1 -0
- package/dist/broker/endpoint-registry.js +83 -0
- package/dist/broker/endpoint-state-machine.d.ts +40 -0
- package/dist/broker/endpoint-state-machine.d.ts.map +1 -0
- package/dist/broker/endpoint-state-machine.js +92 -0
- package/dist/broker/entry.d.ts +5 -0
- package/dist/broker/entry.d.ts.map +1 -0
- package/dist/broker/entry.js +123 -0
- package/dist/broker/grace-timer.d.ts +9 -0
- package/dist/broker/grace-timer.d.ts.map +1 -0
- package/dist/broker/grace-timer.js +34 -0
- package/dist/broker/ipc-server.d.ts +72 -0
- package/dist/broker/ipc-server.d.ts.map +1 -0
- package/dist/broker/ipc-server.js +243 -0
- package/dist/broker/logger.d.ts +10 -0
- package/dist/broker/logger.d.ts.map +1 -0
- package/dist/broker/logger.js +34 -0
- package/dist/broker/paths.d.ts +11 -0
- package/dist/broker/paths.d.ts.map +1 -0
- package/dist/broker/paths.js +30 -0
- package/dist/broker/plugin-liveness.d.ts +2 -0
- package/dist/broker/plugin-liveness.d.ts.map +1 -0
- package/dist/broker/plugin-liveness.js +15 -0
- package/dist/broker/reconnecting-buffer.d.ts +23 -0
- package/dist/broker/reconnecting-buffer.d.ts.map +1 -0
- package/dist/broker/reconnecting-buffer.js +107 -0
- package/dist/broker/routing-table.d.ts +22 -0
- package/dist/broker/routing-table.d.ts.map +1 -0
- package/dist/broker/routing-table.js +35 -0
- package/dist/broker/runtime-endpoint-port.d.ts +8 -0
- package/dist/broker/runtime-endpoint-port.d.ts.map +1 -0
- package/dist/broker/runtime-endpoint-port.js +1 -0
- package/dist/broker/undispatched-inbox.d.ts +26 -0
- package/dist/broker/undispatched-inbox.d.ts.map +1 -0
- package/dist/broker/undispatched-inbox.js +53 -0
- package/dist/broker/version-handshake.d.ts +30 -0
- package/dist/broker/version-handshake.d.ts.map +1 -0
- package/dist/broker/version-handshake.js +47 -0
- package/dist/broker-client/broker-client.d.ts +64 -0
- package/dist/broker-client/broker-client.d.ts.map +1 -0
- package/dist/broker-client/broker-client.js +164 -0
- package/dist/broker-client/lazy-spawn.d.ts +18 -0
- package/dist/broker-client/lazy-spawn.d.ts.map +1 -0
- package/dist/broker-client/lazy-spawn.js +61 -0
- package/dist/runtime-endpoint-client.d.ts +9 -2
- package/dist/runtime-endpoint-client.d.ts.map +1 -1
- package/dist/runtime-endpoint-client.js +15 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +11 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { readConfig } from "../config-fs.js";
|
|
3
|
+
import { DEFAULT_API_HOST } from "../platform-client.js";
|
|
4
|
+
import { heartbeatRuntimeEndpoint, registerRuntimeEndpoint, transitionRuntimeEndpointState, unregisterRuntimeEndpoint, } from "../runtime-endpoint-client.js";
|
|
5
|
+
import { startBrokerDaemon } from "./broker-daemon.js";
|
|
6
|
+
import { ConnectorWS } from "./connector-ws.js";
|
|
7
|
+
import { createBrokerLogger } from "./logger.js";
|
|
8
|
+
import { resolveBrokerPaths } from "./paths.js";
|
|
9
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
10
|
+
async function resolveCredentials(accountId, envApiHost) {
|
|
11
|
+
const config = await readConfig();
|
|
12
|
+
const channels = config.channels;
|
|
13
|
+
const network = channels?.["agent-network"];
|
|
14
|
+
const apiHost = envApiHost ??
|
|
15
|
+
(typeof network?.apiHost === "string" ? network.apiHost : DEFAULT_API_HOST);
|
|
16
|
+
const accounts = network?.accounts;
|
|
17
|
+
const account = accounts?.[accountId];
|
|
18
|
+
const connectorUrl = typeof account?.connectorUrl === "string" ? account.connectorUrl : null;
|
|
19
|
+
const token = typeof account?.token === "string" ? account.token : null;
|
|
20
|
+
if (!connectorUrl || !token) {
|
|
21
|
+
throw new Error(`agent-network credentials missing for account "${accountId}"; ` +
|
|
22
|
+
"run setup before lazy-spawning the broker");
|
|
23
|
+
}
|
|
24
|
+
return { accountId, connectorUrl, token, apiHost };
|
|
25
|
+
}
|
|
26
|
+
export function buildApiPort(apiHost, runtimeKey, logger) {
|
|
27
|
+
return {
|
|
28
|
+
async register(params) {
|
|
29
|
+
return registerRuntimeEndpoint({ apiHost }, runtimeKey, params);
|
|
30
|
+
},
|
|
31
|
+
async heartbeat(endpointId) {
|
|
32
|
+
await heartbeatRuntimeEndpoint({ apiHost }, runtimeKey, endpointId);
|
|
33
|
+
},
|
|
34
|
+
async unregister(endpointId, asNodeId) {
|
|
35
|
+
await unregisterRuntimeEndpoint({ apiHost }, runtimeKey, endpointId, asNodeId);
|
|
36
|
+
},
|
|
37
|
+
async transitionState(endpointId, params) {
|
|
38
|
+
try {
|
|
39
|
+
await transitionRuntimeEndpointState({ apiHost }, runtimeKey, endpointId, params);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const status = errorStatus(err);
|
|
43
|
+
if (status === 404) {
|
|
44
|
+
logger.info("transitionState_404_degraded", {
|
|
45
|
+
endpoint_id: endpointId,
|
|
46
|
+
state: params.state,
|
|
47
|
+
reason: params.reason,
|
|
48
|
+
note: "apps/api state endpoint not yet shipped (PR-B1 in flight); state propagation skipped",
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await sleep(500);
|
|
53
|
+
try {
|
|
54
|
+
await transitionRuntimeEndpointState({ apiHost }, runtimeKey, endpointId, params);
|
|
55
|
+
}
|
|
56
|
+
catch (err2) {
|
|
57
|
+
logger.warn("transitionState_failed", {
|
|
58
|
+
endpoint_id: endpointId,
|
|
59
|
+
state: params.state,
|
|
60
|
+
reason: params.reason,
|
|
61
|
+
err: err2 instanceof Error ? err2.message : String(err2),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function errorStatus(err) {
|
|
69
|
+
if (typeof err !== "object" || err === null)
|
|
70
|
+
return null;
|
|
71
|
+
const status = err.status;
|
|
72
|
+
return typeof status === "number" ? status : null;
|
|
73
|
+
}
|
|
74
|
+
function sleep(ms) {
|
|
75
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
76
|
+
}
|
|
77
|
+
export async function main() {
|
|
78
|
+
const accountId = process.env.MASONS_BROKER_ACCOUNT_ID ?? DEFAULT_ACCOUNT_ID;
|
|
79
|
+
const envApiHost = process.env.MASONS_BROKER_API_HOST;
|
|
80
|
+
const userDataOverride = process.env.MASONS_BROKER_USER_DATA;
|
|
81
|
+
const creds = await resolveCredentials(accountId, envApiHost);
|
|
82
|
+
const paths = resolveBrokerPaths(creds.accountId, creds.token, {
|
|
83
|
+
userDataDir: userDataOverride,
|
|
84
|
+
});
|
|
85
|
+
const logger = createBrokerLogger(paths.logDir);
|
|
86
|
+
logger.info("broker_entry_starting", {
|
|
87
|
+
accountId: creds.accountId,
|
|
88
|
+
apiHost: creds.apiHost,
|
|
89
|
+
pid: process.pid,
|
|
90
|
+
});
|
|
91
|
+
const asNodeId = creds.accountId;
|
|
92
|
+
const connector = new ConnectorWS({
|
|
93
|
+
url: creds.connectorUrl,
|
|
94
|
+
token: creds.token,
|
|
95
|
+
});
|
|
96
|
+
const apiPort = buildApiPort(creds.apiHost, creds.token, logger);
|
|
97
|
+
const broker = await startBrokerDaemon({
|
|
98
|
+
paths,
|
|
99
|
+
asNodeId,
|
|
100
|
+
connector,
|
|
101
|
+
apiPort,
|
|
102
|
+
logger,
|
|
103
|
+
});
|
|
104
|
+
let shuttingDown = false;
|
|
105
|
+
const handleSignal = (sig) => async () => {
|
|
106
|
+
if (shuttingDown)
|
|
107
|
+
return;
|
|
108
|
+
shuttingDown = true;
|
|
109
|
+
logger.info("broker_signal", { sig });
|
|
110
|
+
await broker.shutdown(`signal:${sig}`);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
};
|
|
113
|
+
process.on("SIGTERM", handleSignal("SIGTERM"));
|
|
114
|
+
process.on("SIGINT", handleSignal("SIGINT"));
|
|
115
|
+
logger.info("broker_ready", { ipcUrl: broker.ipcUrl });
|
|
116
|
+
}
|
|
117
|
+
const argvUrl = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
118
|
+
if (import.meta.url === argvUrl) {
|
|
119
|
+
main().catch((err) => {
|
|
120
|
+
console.error("broker_entry_fatal", err);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface GraceTimerManager {
|
|
2
|
+
start(endpoint_id: string, ms: number, onExpire: () => void): void;
|
|
3
|
+
cancel(endpoint_id: string): void;
|
|
4
|
+
has(endpoint_id: string): boolean;
|
|
5
|
+
cancelAll(): void;
|
|
6
|
+
active(): readonly string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function createGraceTimerManager(): GraceTimerManager;
|
|
9
|
+
//# sourceMappingURL=grace-timer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grace-timer.d.ts","sourceRoot":"","sources":["../../src/broker/grace-timer.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAGhC,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAEnE,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAElC,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC;IAElC,SAAS,IAAI,IAAI,CAAC;IAElB,MAAM,IAAI,SAAS,MAAM,EAAE,CAAC;CAC7B;AAED,wBAAgB,uBAAuB,IAAI,iBAAiB,CAmC3D"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function createGraceTimerManager() {
|
|
2
|
+
const timers = new Map();
|
|
3
|
+
return {
|
|
4
|
+
start(endpoint_id, ms, onExpire) {
|
|
5
|
+
const existing = timers.get(endpoint_id);
|
|
6
|
+
if (existing)
|
|
7
|
+
clearTimeout(existing);
|
|
8
|
+
const handle = setTimeout(() => {
|
|
9
|
+
timers.delete(endpoint_id);
|
|
10
|
+
onExpire();
|
|
11
|
+
}, ms);
|
|
12
|
+
handle.unref?.();
|
|
13
|
+
timers.set(endpoint_id, handle);
|
|
14
|
+
},
|
|
15
|
+
cancel(endpoint_id) {
|
|
16
|
+
const handle = timers.get(endpoint_id);
|
|
17
|
+
if (handle) {
|
|
18
|
+
clearTimeout(handle);
|
|
19
|
+
timers.delete(endpoint_id);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
has(endpoint_id) {
|
|
23
|
+
return timers.has(endpoint_id);
|
|
24
|
+
},
|
|
25
|
+
cancelAll() {
|
|
26
|
+
for (const [, handle] of timers)
|
|
27
|
+
clearTimeout(handle);
|
|
28
|
+
timers.clear();
|
|
29
|
+
},
|
|
30
|
+
active() {
|
|
31
|
+
return Array.from(timers.keys());
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type RawData, WebSocket } from "ws";
|
|
2
|
+
import type { BrokerLogger } from "./logger.js";
|
|
3
|
+
export interface IPCServerHandlers {
|
|
4
|
+
registerEndpoint(body: RegisterEndpointBody, ipcWs: WebSocket): Promise<RegisterEndpointResponse>;
|
|
5
|
+
heartbeatEndpoint(endpoint_id: string): Promise<void>;
|
|
6
|
+
unregisterEndpoint(endpoint_id: string): Promise<void>;
|
|
7
|
+
send(body: SendBody): Promise<SendResponse>;
|
|
8
|
+
reattachEndpoint(endpoint_id: string, plugin_pid: number, ipcWs: WebSocket): Promise<ReattachResponse>;
|
|
9
|
+
listUndispatched(): Promise<UndispatchedListResponse>;
|
|
10
|
+
dispatch(undispatched_id: string, target_endpoint_id: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export interface ReattachResponse {
|
|
13
|
+
restored: true;
|
|
14
|
+
reconnecting_buffer_count: number;
|
|
15
|
+
}
|
|
16
|
+
export type UndispatchedListResponse = import("./undispatched-inbox.js").UndispatchedMessage[];
|
|
17
|
+
export interface RegisterEndpointBody {
|
|
18
|
+
agent_id: string;
|
|
19
|
+
plugin_pid: number;
|
|
20
|
+
kind: string;
|
|
21
|
+
started_at?: string;
|
|
22
|
+
workspace?: string;
|
|
23
|
+
tracking_ref?: string;
|
|
24
|
+
session_name?: string;
|
|
25
|
+
task_hint?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RegisterEndpointResponse {
|
|
28
|
+
endpoint_id: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SendBody {
|
|
31
|
+
endpoint_id: string;
|
|
32
|
+
to: string;
|
|
33
|
+
content: string;
|
|
34
|
+
contentType?: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
require_live?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface SendResponse {
|
|
39
|
+
messageId: string;
|
|
40
|
+
status: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class BrokerHttpError extends Error {
|
|
43
|
+
readonly status: number;
|
|
44
|
+
readonly code: string;
|
|
45
|
+
constructor(status: number, code: string, message: string);
|
|
46
|
+
}
|
|
47
|
+
export interface IPCServerOptions {
|
|
48
|
+
bearerToken: string;
|
|
49
|
+
handlers: IPCServerHandlers;
|
|
50
|
+
logger: BrokerLogger;
|
|
51
|
+
onChannelOpened?: (plugin_pid: number, ws: WebSocket) => void;
|
|
52
|
+
onChannelClosed?: (plugin_pid: number, ws: WebSocket) => void;
|
|
53
|
+
}
|
|
54
|
+
export interface RunningIPCServer {
|
|
55
|
+
ipcUrl: string;
|
|
56
|
+
port: number;
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export declare function startIPCServer(opts: IPCServerOptions): Promise<RunningIPCServer>;
|
|
60
|
+
export declare function pushToPlugin(ws: WebSocket, event: PushEvent): void;
|
|
61
|
+
export type PushEvent = {
|
|
62
|
+
event: "message_received";
|
|
63
|
+
from: string;
|
|
64
|
+
content: string;
|
|
65
|
+
contentType: string;
|
|
66
|
+
metadata?: Record<string, unknown>;
|
|
67
|
+
} | {
|
|
68
|
+
event: "ping";
|
|
69
|
+
ts: string;
|
|
70
|
+
};
|
|
71
|
+
export declare function parsePluginFrame(data: RawData): unknown;
|
|
72
|
+
//# sourceMappingURL=ipc-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipc-server.d.ts","sourceRoot":"","sources":["../../src/broker/ipc-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
3
|
+
import { PLUGIN_VERSION } from "../version.js";
|
|
4
|
+
import { handleInitialize, IPC_PROTOCOL_VERSION, SERVER_CAPABILITIES, } from "./version-handshake.js";
|
|
5
|
+
export class BrokerHttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
constructor(status, code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "BrokerHttpError";
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function startIPCServer(opts) {
|
|
16
|
+
const { bearerToken, handlers, logger, onChannelOpened, onChannelClosed } = opts;
|
|
17
|
+
const http = createServer((req, res) => {
|
|
18
|
+
void routeHttp(req, res, bearerToken, handlers, logger);
|
|
19
|
+
});
|
|
20
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
21
|
+
http.on("upgrade", (req, socket, head) => {
|
|
22
|
+
if (req.url !== "/v1/stream") {
|
|
23
|
+
socket.write("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
24
|
+
socket.destroy();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!authorizedRequest(req, bearerToken)) {
|
|
28
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
29
|
+
socket.destroy();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const pid = readPluginPid(req);
|
|
33
|
+
if (pid === null) {
|
|
34
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
35
|
+
socket.destroy();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
39
|
+
onChannelOpened?.(pid, ws);
|
|
40
|
+
ws.once("close", () => onChannelClosed?.(pid, ws));
|
|
41
|
+
wss.emit("connection", ws, req);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
http.once("error", reject);
|
|
46
|
+
http.listen(0, "127.0.0.1", () => {
|
|
47
|
+
http.off("error", reject);
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
const addr = http.address();
|
|
52
|
+
const ipcUrl = `http://127.0.0.1:${addr.port}`;
|
|
53
|
+
logger.info("ipc_server_listening", { ipcUrl });
|
|
54
|
+
return {
|
|
55
|
+
ipcUrl,
|
|
56
|
+
port: addr.port,
|
|
57
|
+
async close() {
|
|
58
|
+
await closeServer(http, wss);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function routeHttp(req, res, bearerToken, handlers, logger) {
|
|
63
|
+
const url = req.url ?? "";
|
|
64
|
+
const method = req.method ?? "GET";
|
|
65
|
+
if (method === "GET" && url === "/health") {
|
|
66
|
+
sendJson(res, 200, {
|
|
67
|
+
ok: true,
|
|
68
|
+
version: PLUGIN_VERSION,
|
|
69
|
+
protocol_version: IPC_PROTOCOL_VERSION,
|
|
70
|
+
capabilities: SERVER_CAPABILITIES,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!authorizedRequest(req, bearerToken)) {
|
|
75
|
+
sendJson(res, 401, { error: "unauthorized" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
if (method === "POST" && url === "/v1/initialize") {
|
|
80
|
+
const body = await readJson(req);
|
|
81
|
+
const result = handleInitialize(body);
|
|
82
|
+
sendJson(res, result.status, result.body);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (method === "POST" && url === "/v1/endpoint/register") {
|
|
86
|
+
const body = await readJson(req);
|
|
87
|
+
const out = await handlers.registerEndpoint(body, null);
|
|
88
|
+
sendJson(res, 200, out);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const heartbeatMatch = /^\/v1\/endpoint\/([^/]+)\/heartbeat$/.exec(url);
|
|
92
|
+
if (method === "POST" && heartbeatMatch && heartbeatMatch[1]) {
|
|
93
|
+
await handlers.heartbeatEndpoint(decodeURIComponent(heartbeatMatch[1]));
|
|
94
|
+
sendJson(res, 200, { ok: true });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const reattachMatch = /^\/v1\/endpoint\/([^/]+)\/reattach$/.exec(url);
|
|
98
|
+
if (method === "POST" && reattachMatch && reattachMatch[1]) {
|
|
99
|
+
const body = await readJson(req);
|
|
100
|
+
const pid = typeof body.plugin_pid === "number" ? body.plugin_pid : Number.NaN;
|
|
101
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
102
|
+
throw new BrokerHttpError(400, "plugin_pid_invalid", "plugin_pid is required and must be a positive number");
|
|
103
|
+
}
|
|
104
|
+
const out = await handlers.reattachEndpoint(decodeURIComponent(reattachMatch[1]), pid, null);
|
|
105
|
+
sendJson(res, 200, out);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const unregisterMatch = /^\/v1\/endpoint\/([^/]+)$/.exec(url);
|
|
109
|
+
if (method === "DELETE" && unregisterMatch && unregisterMatch[1]) {
|
|
110
|
+
await handlers.unregisterEndpoint(decodeURIComponent(unregisterMatch[1]));
|
|
111
|
+
sendJson(res, 200, { ok: true });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (method === "POST" && url === "/v1/send") {
|
|
115
|
+
const body = await readJson(req);
|
|
116
|
+
const out = await handlers.send(body);
|
|
117
|
+
sendJson(res, 200, out);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (method === "GET" && url === "/v1/undispatched") {
|
|
121
|
+
const items = await handlers.listUndispatched();
|
|
122
|
+
sendJson(res, 200, { items });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (method === "POST" && url === "/v1/dispatch") {
|
|
126
|
+
const body = await readJson(req);
|
|
127
|
+
const uid = typeof body.undispatched_id === "string"
|
|
128
|
+
? body.undispatched_id
|
|
129
|
+
: undefined;
|
|
130
|
+
const tid = typeof body.target_endpoint_id === "string"
|
|
131
|
+
? body.target_endpoint_id
|
|
132
|
+
: undefined;
|
|
133
|
+
if (!uid || !tid) {
|
|
134
|
+
throw new BrokerHttpError(400, "dispatch_body_invalid", "POST /v1/dispatch requires { undispatched_id, target_endpoint_id }");
|
|
135
|
+
}
|
|
136
|
+
await handlers.dispatch(uid, tid);
|
|
137
|
+
sendJson(res, 200, { ok: true });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
sendJson(res, 404, { error: "not_found", path: url });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (err instanceof BrokerHttpError) {
|
|
144
|
+
logger.info("ipc_route_rejected", {
|
|
145
|
+
url,
|
|
146
|
+
method,
|
|
147
|
+
status: err.status,
|
|
148
|
+
code: err.code,
|
|
149
|
+
message: err.message,
|
|
150
|
+
});
|
|
151
|
+
sendJson(res, err.status, { error: err.code, message: err.message });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
logger.error("ipc_route_error", {
|
|
155
|
+
url,
|
|
156
|
+
method,
|
|
157
|
+
err: err instanceof Error ? err.message : String(err),
|
|
158
|
+
});
|
|
159
|
+
sendJson(res, 500, {
|
|
160
|
+
error: "internal_error",
|
|
161
|
+
message: err instanceof Error ? err.message : "unknown",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function authorizedRequest(req, bearerToken) {
|
|
166
|
+
const header = req.headers.authorization;
|
|
167
|
+
if (typeof header !== "string")
|
|
168
|
+
return false;
|
|
169
|
+
const trimmed = header.startsWith("Bearer ")
|
|
170
|
+
? header.slice("Bearer ".length)
|
|
171
|
+
: header;
|
|
172
|
+
return constantTimeEquals(trimmed, bearerToken);
|
|
173
|
+
}
|
|
174
|
+
function readPluginPid(req) {
|
|
175
|
+
const raw = req.headers["x-plugin-pid"];
|
|
176
|
+
const str = Array.isArray(raw) ? raw[0] : raw;
|
|
177
|
+
if (!str)
|
|
178
|
+
return null;
|
|
179
|
+
const n = Number.parseInt(str, 10);
|
|
180
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
181
|
+
}
|
|
182
|
+
function constantTimeEquals(a, b) {
|
|
183
|
+
if (a.length !== b.length)
|
|
184
|
+
return false;
|
|
185
|
+
let mismatch = 0;
|
|
186
|
+
for (let i = 0; i < a.length; i++) {
|
|
187
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
188
|
+
}
|
|
189
|
+
return mismatch === 0;
|
|
190
|
+
}
|
|
191
|
+
async function readJson(req) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
const chunks = [];
|
|
194
|
+
let total = 0;
|
|
195
|
+
const MAX = 1 * 1024 * 1024;
|
|
196
|
+
req.on("data", (chunk) => {
|
|
197
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
198
|
+
total += buf.length;
|
|
199
|
+
if (total > MAX) {
|
|
200
|
+
reject(new Error("request body too large"));
|
|
201
|
+
req.destroy();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
chunks.push(buf);
|
|
205
|
+
});
|
|
206
|
+
req.on("end", () => {
|
|
207
|
+
try {
|
|
208
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
209
|
+
resolve(raw.length === 0 ? {} : JSON.parse(raw));
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
req.on("error", reject);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function sendJson(res, status, body) {
|
|
219
|
+
res.statusCode = status;
|
|
220
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
221
|
+
res.end(JSON.stringify(body));
|
|
222
|
+
}
|
|
223
|
+
async function closeServer(http, wss) {
|
|
224
|
+
await new Promise((resolve) => {
|
|
225
|
+
wss.close(() => resolve());
|
|
226
|
+
});
|
|
227
|
+
await new Promise((resolve, reject) => {
|
|
228
|
+
http.close((err) => (err ? reject(err) : resolve()));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
export function pushToPlugin(ws, event) {
|
|
232
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
233
|
+
return;
|
|
234
|
+
ws.send(JSON.stringify(event));
|
|
235
|
+
}
|
|
236
|
+
export function parsePluginFrame(data) {
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(data.toString());
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
export interface BrokerLogger {
|
|
3
|
+
debug(msg: string, fields?: Record<string, unknown>): void;
|
|
4
|
+
info(msg: string, fields?: Record<string, unknown>): void;
|
|
5
|
+
warn(msg: string, fields?: Record<string, unknown>): void;
|
|
6
|
+
error(msg: string, fields?: Record<string, unknown>): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createBrokerLogger(logDir: string): BrokerLogger;
|
|
9
|
+
export declare function createNullLogger(): BrokerLogger;
|
|
10
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/broker/logger.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC3D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CA6B/D;AAGD,wBAAgB,gBAAgB,IAAI,YAAY,CAO/C"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function createBrokerLogger(logDir) {
|
|
4
|
+
mkdirSync(logDir, { recursive: true, mode: 0o700 });
|
|
5
|
+
const path = join(logDir, `broker-${process.pid}.log`);
|
|
6
|
+
const write = (level, msg, fields) => {
|
|
7
|
+
const line = JSON.stringify({
|
|
8
|
+
ts: new Date().toISOString(),
|
|
9
|
+
level,
|
|
10
|
+
pid: process.pid,
|
|
11
|
+
msg,
|
|
12
|
+
...fields,
|
|
13
|
+
});
|
|
14
|
+
try {
|
|
15
|
+
appendFileSync(path, `${line}\n`);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
debug: (msg, fields) => write("debug", msg, fields),
|
|
22
|
+
info: (msg, fields) => write("info", msg, fields),
|
|
23
|
+
warn: (msg, fields) => write("warn", msg, fields),
|
|
24
|
+
error: (msg, fields) => write("error", msg, fields),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function createNullLogger() {
|
|
28
|
+
return {
|
|
29
|
+
debug: () => { },
|
|
30
|
+
info: () => { },
|
|
31
|
+
warn: () => { },
|
|
32
|
+
error: () => { },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BrokerPathOptions {
|
|
2
|
+
userDataDir?: string;
|
|
3
|
+
}
|
|
4
|
+
export interface BrokerPaths {
|
|
5
|
+
discoveryFile: string;
|
|
6
|
+
discoveryDir: string;
|
|
7
|
+
logDir: string;
|
|
8
|
+
buffersDir: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveBrokerPaths(principal: string, runtimeToken: string, opts?: BrokerPathOptions): BrokerPaths;
|
|
11
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/broker/paths.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,iBAAiB;IAEhC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAE1B,aAAa,EAAE,MAAM,CAAC;IAEtB,YAAY,EAAE,MAAM,CAAC;IAErB,MAAM,EAAE,MAAM,CAAC;IAQf,UAAU,EAAE,MAAM,CAAC;CACpB;AASD,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,IAAI,GAAE,iBAAsB,GAC3B,WAAW,CAgCb"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import envPaths from "env-paths";
|
|
4
|
+
const AGENT_IDENTITY_HEX_LEN = 16;
|
|
5
|
+
const ENV_PATHS_NAME = "masons-runtime-broker";
|
|
6
|
+
export function resolveBrokerPaths(principal, runtimeToken, opts = {}) {
|
|
7
|
+
if (!principal) {
|
|
8
|
+
throw new Error("principal must be a non-empty string");
|
|
9
|
+
}
|
|
10
|
+
if (!runtimeToken) {
|
|
11
|
+
throw new Error("runtimeToken must be a non-empty string");
|
|
12
|
+
}
|
|
13
|
+
const userData = opts.userDataDir ?? envPaths(ENV_PATHS_NAME, { suffix: "" }).data;
|
|
14
|
+
const agentIdentity = createHash("sha256")
|
|
15
|
+
.update(runtimeToken, "utf8")
|
|
16
|
+
.digest("hex")
|
|
17
|
+
.slice(0, AGENT_IDENTITY_HEX_LEN);
|
|
18
|
+
const discoveryDir = join(userData, "runtime-broker", sanitizeSegment(principal), agentIdentity);
|
|
19
|
+
const logDir = join(userData, "runtime-broker", "log");
|
|
20
|
+
const buffersDir = join(discoveryDir, "buffers");
|
|
21
|
+
return {
|
|
22
|
+
discoveryFile: join(discoveryDir, "discovery.json"),
|
|
23
|
+
discoveryDir,
|
|
24
|
+
logDir,
|
|
25
|
+
buffersDir,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function sanitizeSegment(segment) {
|
|
29
|
+
return segment.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-liveness.d.ts","sourceRoot":"","sources":["../../src/broker/plugin-liveness.ts"],"names":[],"mappings":"AAyBA,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAiBzD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isPluginAlive(plugin_pid) {
|
|
2
|
+
if (!Number.isFinite(plugin_pid) || plugin_pid <= 0) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
try {
|
|
6
|
+
process.kill(plugin_pid, 0);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
const code = err.code;
|
|
11
|
+
if (code === "ESRCH")
|
|
12
|
+
return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface BufferedMessage {
|
|
2
|
+
id: string;
|
|
3
|
+
arrived_at: string;
|
|
4
|
+
envelope: {
|
|
5
|
+
from: string;
|
|
6
|
+
content: string;
|
|
7
|
+
contentType: string;
|
|
8
|
+
metadata?: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface ReconnectingBuffer {
|
|
12
|
+
append(msg: BufferedMessage): void;
|
|
13
|
+
read(): BufferedMessage[];
|
|
14
|
+
drain(): BufferedMessage[];
|
|
15
|
+
delete(): void;
|
|
16
|
+
size(): number;
|
|
17
|
+
}
|
|
18
|
+
export interface ReconnectingBufferManager {
|
|
19
|
+
forEndpoint(endpoint_id: string): ReconnectingBuffer;
|
|
20
|
+
cleanup(endpoint_id: string): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createReconnectingBufferManager(buffersDir: string): ReconnectingBufferManager;
|
|
23
|
+
//# sourceMappingURL=reconnecting-buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconnecting-buffer.d.ts","sourceRoot":"","sources":["../../src/broker/reconnecting-buffer.ts"],"names":[],"mappings":"AA+CA,MAAM,WAAW,eAAe;IAE9B,EAAE,EAAE,MAAM,CAAC;IAEX,UAAU,EAAE,MAAM,CAAC;IAEnB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IAEjC,MAAM,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CAAC;IAEnC,IAAI,IAAI,eAAe,EAAE,CAAC;IAE1B,KAAK,IAAI,eAAe,EAAE,CAAC;IAE3B,MAAM,IAAI,IAAI,CAAC;IAEf,IAAI,IAAI,MAAM,CAAC;CAChB;AA8ED,MAAM,WAAW,yBAAyB;IAExC,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAAC;IAErD,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,MAAM,GACjB,yBAAyB,CAmB3B"}
|