@nekzus/liop 1.2.0-alpha.9 → 1.2.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/README.md +12 -3
- package/dist/bin/agent.js +222 -51
- package/dist/bridge/index.js +7 -6
- package/dist/bridge/stream.js +11 -11
- package/dist/client/index.js +46 -35
- package/dist/crypto/logic-image-id.d.ts +3 -0
- package/dist/crypto/logic-image-id.js +27 -0
- package/dist/crypto/verifier.js +7 -19
- package/dist/economy/estimator.d.ts +53 -0
- package/dist/economy/estimator.js +69 -0
- package/dist/economy/index.d.ts +5 -0
- package/dist/economy/index.js +3 -0
- package/dist/economy/otel.d.ts +38 -0
- package/dist/economy/otel.js +100 -0
- package/dist/economy/telemetry.d.ts +77 -0
- package/dist/economy/telemetry.js +224 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +19 -0
- package/dist/gateway/hybrid.d.ts +3 -1
- package/dist/gateway/hybrid.js +38 -13
- package/dist/gateway/router.d.ts +25 -9
- package/dist/gateway/router.js +484 -133
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mesh/node.d.ts +16 -0
- package/dist/mesh/node.js +394 -113
- package/dist/prompts/adapters.d.ts +16 -0
- package/dist/prompts/adapters.js +55 -0
- package/dist/rpc/proto.js +2 -1
- package/dist/rpc/server.d.ts +1 -1
- package/dist/rpc/server.js +4 -3
- package/dist/rpc/tls.js +3 -2
- package/dist/sandbox/wasi.d.ts +1 -1
- package/dist/sandbox/wasi.js +43 -3
- package/dist/security/guardian.js +3 -2
- package/dist/security/zk.d.ts +2 -3
- package/dist/security/zk.js +22 -9
- package/dist/server/index.d.ts +53 -4
- package/dist/server/index.js +362 -49
- package/dist/server/pii.d.ts +12 -0
- package/dist/server/pii.js +90 -0
- package/dist/types.d.ts +16 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.js +70 -0
- package/dist/utils/mcpCompact.d.ts +11 -0
- package/dist/utils/mcpCompact.js +29 -0
- package/dist/workers/logic-execution.d.ts +1 -1
- package/dist/workers/logic-execution.js +38 -20
- package/dist/workers/zk-verifier.js +37 -33
- package/package.json +14 -2
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createSyncTokenEstimator, createTokenEstimator, } from "./estimator.js";
|
|
2
|
+
import { LiopOTelBridge } from "./otel.js";
|
|
3
|
+
/**
|
|
4
|
+
* Maps operation types to OTel gen_ai.operation.name values.
|
|
5
|
+
* @see https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
6
|
+
*/
|
|
7
|
+
const OTEL_OPERATION_MAP = {
|
|
8
|
+
tools_list: "chat",
|
|
9
|
+
tool_call: "execute_tool",
|
|
10
|
+
resource_read: "chat",
|
|
11
|
+
resource_list: "chat",
|
|
12
|
+
prompt_get: "chat",
|
|
13
|
+
prompt_list: "chat",
|
|
14
|
+
diagnostic: "chat",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* TokenTelemetryEngine — Full-spectrum observational singleton for token cost measurement.
|
|
18
|
+
*
|
|
19
|
+
* Design principles:
|
|
20
|
+
* - Pure observer pattern: NEVER mutates MCP payloads or protocol flow.
|
|
21
|
+
* - Real tokenization: o200k_base BPE via gpt-tokenizer (async init, sync counting).
|
|
22
|
+
* - OTel gen_ai.* emission: standard metrics via @opentelemetry/api (NoOp if no provider).
|
|
23
|
+
* - Error isolation: telemetry failures never propagate to protocol operations.
|
|
24
|
+
*/
|
|
25
|
+
export class TokenTelemetryEngine {
|
|
26
|
+
static instance = null;
|
|
27
|
+
operations = [];
|
|
28
|
+
sessionId;
|
|
29
|
+
startedAt;
|
|
30
|
+
estimator;
|
|
31
|
+
otelBridge;
|
|
32
|
+
constructor() {
|
|
33
|
+
this.sessionId = crypto.randomUUID();
|
|
34
|
+
this.startedAt = Date.now();
|
|
35
|
+
// Start with sync heuristic estimator (available immediately)
|
|
36
|
+
this.estimator = createSyncTokenEstimator();
|
|
37
|
+
this.otelBridge = new LiopOTelBridge();
|
|
38
|
+
// Upgrade to real tokenizer asynchronously
|
|
39
|
+
this.initRealEstimator();
|
|
40
|
+
}
|
|
41
|
+
/** Async upgrade from heuristic to real BPE tokenizer */
|
|
42
|
+
initRealEstimator() {
|
|
43
|
+
createTokenEstimator()
|
|
44
|
+
.then((real) => {
|
|
45
|
+
this.estimator = real;
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
// Keep heuristic fallback — already assigned in constructor
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
static getInstance() {
|
|
52
|
+
if (!TokenTelemetryEngine.instance) {
|
|
53
|
+
TokenTelemetryEngine.instance = new TokenTelemetryEngine();
|
|
54
|
+
}
|
|
55
|
+
return TokenTelemetryEngine.instance;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Count tokens in a string using the active estimator.
|
|
59
|
+
* Delegates to o200k_base BPE tokenizer (or heuristic fallback).
|
|
60
|
+
*/
|
|
61
|
+
countTokens(content) {
|
|
62
|
+
try {
|
|
63
|
+
return this.estimator.countTokens(content);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Fallback: never let counting failures break protocol flow
|
|
67
|
+
return Math.ceil(content.length / 4);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Record a single MCP operation's token footprint.
|
|
72
|
+
* Emits both internal metrics and OTel gen_ai.* histograms.
|
|
73
|
+
*/
|
|
74
|
+
record(metric) {
|
|
75
|
+
const fullMetric = {
|
|
76
|
+
...metric,
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
this.operations.push(fullMetric);
|
|
80
|
+
// Emit to OTel bridge (NoOp if no MeterProvider configured)
|
|
81
|
+
try {
|
|
82
|
+
const otelOp = OTEL_OPERATION_MAP[metric.type] || "chat";
|
|
83
|
+
if (metric.estimatedInputTokens > 0) {
|
|
84
|
+
this.otelBridge.recordTokens(metric.estimatedInputTokens, "input", otelOp, metric.toolName);
|
|
85
|
+
}
|
|
86
|
+
if (metric.estimatedOutputTokens > 0) {
|
|
87
|
+
this.otelBridge.recordTokens(metric.estimatedOutputTokens, "output", otelOp, metric.toolName);
|
|
88
|
+
}
|
|
89
|
+
if (metric.durationMs !== undefined) {
|
|
90
|
+
this.otelBridge.recordDuration(metric.durationMs, otelOp);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// OTel emission failure must never affect protocol operations
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* @deprecated Use countTokens() instead. Kept for backward compatibility.
|
|
99
|
+
*/
|
|
100
|
+
estimateTokens(content) {
|
|
101
|
+
return this.countTokens(content);
|
|
102
|
+
}
|
|
103
|
+
/** Generate the full session report */
|
|
104
|
+
getReport() {
|
|
105
|
+
return {
|
|
106
|
+
sessionId: this.sessionId,
|
|
107
|
+
operations: [...this.operations],
|
|
108
|
+
totalInputTokens: this.operations.reduce((sum, op) => sum + op.estimatedInputTokens, 0),
|
|
109
|
+
totalOutputTokens: this.operations.reduce((sum, op) => sum + op.estimatedOutputTokens, 0),
|
|
110
|
+
estimatorName: this.estimator.name,
|
|
111
|
+
sessionUptimeMs: Date.now() - this.startedAt,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Get per-tool token breakdown for diagnostic display */
|
|
115
|
+
getPerToolReport() {
|
|
116
|
+
const breakdown = new Map();
|
|
117
|
+
for (const op of this.operations) {
|
|
118
|
+
const key = op.toolName || op.method;
|
|
119
|
+
const existing = breakdown.get(key) || {
|
|
120
|
+
input: 0,
|
|
121
|
+
output: 0,
|
|
122
|
+
calls: 0,
|
|
123
|
+
avgDurationMs: 0,
|
|
124
|
+
};
|
|
125
|
+
const totalDuration = existing.avgDurationMs * existing.calls + (op.durationMs || 0);
|
|
126
|
+
const newCalls = existing.calls + 1;
|
|
127
|
+
breakdown.set(key, {
|
|
128
|
+
input: existing.input + op.estimatedInputTokens,
|
|
129
|
+
output: existing.output + op.estimatedOutputTokens,
|
|
130
|
+
calls: newCalls,
|
|
131
|
+
avgDurationMs: newCalls > 0 ? totalDuration / newCalls : 0,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return breakdown;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Format a rich, human-readable summary block for LiopMeshStatus diagnostic.
|
|
138
|
+
* Returns empty string when no operations have been recorded.
|
|
139
|
+
*/
|
|
140
|
+
formatStatusBlock() {
|
|
141
|
+
const report = this.getReport();
|
|
142
|
+
if (report.operations.length === 0)
|
|
143
|
+
return "";
|
|
144
|
+
const uptimeStr = this.formatUptime(report.sessionUptimeMs);
|
|
145
|
+
const totalCombined = report.totalInputTokens + report.totalOutputTokens;
|
|
146
|
+
// Aggregate operations by type
|
|
147
|
+
const byType = new Map();
|
|
148
|
+
for (const op of report.operations) {
|
|
149
|
+
const key = op.type;
|
|
150
|
+
const existing = byType.get(key) || {
|
|
151
|
+
count: 0,
|
|
152
|
+
input: 0,
|
|
153
|
+
output: 0,
|
|
154
|
+
};
|
|
155
|
+
byType.set(key, {
|
|
156
|
+
count: existing.count + 1,
|
|
157
|
+
input: existing.input + op.estimatedInputTokens,
|
|
158
|
+
output: existing.output + op.estimatedOutputTokens,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Build type breakdown lines
|
|
162
|
+
const typeEntries = Array.from(byType.entries());
|
|
163
|
+
const typeLines = typeEntries.map(([type, data], idx) => {
|
|
164
|
+
const prefix = idx === typeEntries.length - 1 ? "│ └─" : "│ ├─";
|
|
165
|
+
const outputPart = data.output > 0 ? ` / ${data.output.toLocaleString()} out` : "";
|
|
166
|
+
return `${prefix} ${type} ×${data.count} → ${data.input.toLocaleString()} in${outputPart}`;
|
|
167
|
+
});
|
|
168
|
+
// Build per-tool breakdown
|
|
169
|
+
const toolReport = this.getPerToolReport();
|
|
170
|
+
const toolEntries = Array.from(toolReport.entries()).filter(([key]) => key !== "tools/list" && key !== "LiopMeshStatus");
|
|
171
|
+
const toolLines = [];
|
|
172
|
+
if (toolEntries.length > 0) {
|
|
173
|
+
toolLines.push("├─ By Tool:");
|
|
174
|
+
toolEntries.forEach(([name, data], idx) => {
|
|
175
|
+
const prefix = idx === toolEntries.length - 1 ? "│ └─" : "│ ├─";
|
|
176
|
+
const outputPart = data.output > 0 ? ` / ${data.output.toLocaleString()} out` : "";
|
|
177
|
+
const durationPart = data.avgDurationMs > 0 ? ` ~${Math.round(data.avgDurationMs)}ms` : "";
|
|
178
|
+
toolLines.push(`${prefix} ${name}: ${data.input.toLocaleString()} in${outputPart} (×${data.calls})${durationPart}`);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// Calculate average latency across all timed operations
|
|
182
|
+
const timedOps = report.operations.filter((op) => op.durationMs !== undefined);
|
|
183
|
+
const avgLatency = timedOps.length > 0
|
|
184
|
+
? Math.round(timedOps.reduce((sum, op) => sum + (op.durationMs || 0), 0) /
|
|
185
|
+
timedOps.length)
|
|
186
|
+
: 0;
|
|
187
|
+
const otelStatus = this.otelBridge.isActive()
|
|
188
|
+
? "gen_ai.client.token.usage → active"
|
|
189
|
+
: "disabled";
|
|
190
|
+
const lines = [
|
|
191
|
+
"\nToken Economy:",
|
|
192
|
+
`├─ Session: ${report.sessionId.slice(0, 8)} (${uptimeStr})`,
|
|
193
|
+
`├─ Estimator: ${report.estimatorName}`,
|
|
194
|
+
`├─ Operations: ${report.operations.length}`,
|
|
195
|
+
...typeLines,
|
|
196
|
+
`├─ Total: ${report.totalInputTokens.toLocaleString()} in / ${report.totalOutputTokens.toLocaleString()} out (${totalCombined.toLocaleString()} combined)`,
|
|
197
|
+
...toolLines,
|
|
198
|
+
...(avgLatency > 0 ? [`├─ Avg Latency: ${avgLatency}ms`] : []),
|
|
199
|
+
`└─ OTel: ${otelStatus}`,
|
|
200
|
+
];
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
/** Format milliseconds into human-readable uptime string */
|
|
204
|
+
formatUptime(ms) {
|
|
205
|
+
const seconds = Math.floor(ms / 1000);
|
|
206
|
+
if (seconds < 60)
|
|
207
|
+
return `${seconds}s`;
|
|
208
|
+
const minutes = Math.floor(seconds / 60);
|
|
209
|
+
const remainingSeconds = seconds % 60;
|
|
210
|
+
if (minutes < 60)
|
|
211
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
212
|
+
const hours = Math.floor(minutes / 60);
|
|
213
|
+
const remainingMinutes = minutes % 60;
|
|
214
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
215
|
+
}
|
|
216
|
+
/** Reset all recorded metrics (used in tests) */
|
|
217
|
+
reset() {
|
|
218
|
+
this.operations = [];
|
|
219
|
+
}
|
|
220
|
+
/** Destroy the singleton (used in tests to guarantee isolation) */
|
|
221
|
+
static destroy() {
|
|
222
|
+
TokenTelemetryEngine.instance = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare enum ErrorCode {
|
|
2
|
+
CapabilityViolation = "CapabilityViolation",
|
|
3
|
+
SandboxEscape = "SandboxEscape",
|
|
4
|
+
PiiLeak = "PiiLeak",
|
|
5
|
+
InvalidIntent = "InvalidIntent",
|
|
6
|
+
Throttled = "Throttled",
|
|
7
|
+
ZkVerificationFailed = "ZkVerificationFailed",
|
|
8
|
+
MeshUnavailable = "MeshUnavailable",
|
|
9
|
+
ConnectionFailed = "ConnectionFailed"
|
|
10
|
+
}
|
|
11
|
+
export declare class LiopError extends Error {
|
|
12
|
+
readonly code: ErrorCode;
|
|
13
|
+
constructor(code: ErrorCode, message: string);
|
|
14
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export var ErrorCode;
|
|
2
|
+
(function (ErrorCode) {
|
|
3
|
+
ErrorCode["CapabilityViolation"] = "CapabilityViolation";
|
|
4
|
+
ErrorCode["SandboxEscape"] = "SandboxEscape";
|
|
5
|
+
ErrorCode["PiiLeak"] = "PiiLeak";
|
|
6
|
+
ErrorCode["InvalidIntent"] = "InvalidIntent";
|
|
7
|
+
ErrorCode["Throttled"] = "Throttled";
|
|
8
|
+
ErrorCode["ZkVerificationFailed"] = "ZkVerificationFailed";
|
|
9
|
+
ErrorCode["MeshUnavailable"] = "MeshUnavailable";
|
|
10
|
+
ErrorCode["ConnectionFailed"] = "ConnectionFailed";
|
|
11
|
+
})(ErrorCode || (ErrorCode = {}));
|
|
12
|
+
export class LiopError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code, message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "LiopError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/dist/gateway/hybrid.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MeshNode } from "../mesh/index.js";
|
|
2
2
|
import type { LiopServer } from "../server/index.js";
|
|
3
|
+
import { LiopMcpRouter } from "./router.js";
|
|
3
4
|
/**
|
|
4
5
|
* LIOP Hybrid Gateway
|
|
5
6
|
* High-level orchestration for connecting MCP (JSON-RPC) clients to the LIOP Mesh.
|
|
@@ -16,6 +17,7 @@ export declare class LiopHybridGateway {
|
|
|
16
17
|
private setupH1Routes;
|
|
17
18
|
private handleGrpcStream;
|
|
18
19
|
private handleMcpH2Stream;
|
|
19
|
-
listen(port: number, host?: string): Promise<
|
|
20
|
+
listen(port: number, host?: string): Promise<number>;
|
|
20
21
|
stop(): Promise<void>;
|
|
22
|
+
getRouter(): LiopMcpRouter;
|
|
21
23
|
}
|
package/dist/gateway/hybrid.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
import * as http2 from "node:http2";
|
|
3
3
|
import * as net from "node:net";
|
|
4
|
+
import { log } from "../utils/logger.js";
|
|
4
5
|
import { LiopMcpRouter } from "./router.js";
|
|
5
6
|
/**
|
|
6
7
|
* LIOP Hybrid Gateway
|
|
@@ -28,7 +29,7 @@ export class LiopHybridGateway {
|
|
|
28
29
|
this.netServer = net.createServer((socket) => {
|
|
29
30
|
socket.once("data", (buffer) => {
|
|
30
31
|
const isHttp2 = buffer.toString().startsWith("PRI * HTTP/2.0");
|
|
31
|
-
|
|
32
|
+
log.info(`[LIOP-Gateway] Incoming L4 Connection. Protocol: ${isHttp2 ? "HTTP/2 (gRPC)" : "HTTP/1.1 (MCP)"}`);
|
|
32
33
|
if (isHttp2) {
|
|
33
34
|
this.h2Server.emit("connection", socket);
|
|
34
35
|
}
|
|
@@ -37,12 +38,12 @@ export class LiopHybridGateway {
|
|
|
37
38
|
}
|
|
38
39
|
socket.unshift(buffer);
|
|
39
40
|
});
|
|
40
|
-
socket.on("error", (err) =>
|
|
41
|
+
socket.on("error", (err) => log.error(`[LIOP-Gateway] NetServer Socket Error: ${err.message}`));
|
|
41
42
|
});
|
|
42
43
|
// Attach error listeners to sub-servers to catch silent failures
|
|
43
|
-
this.h1Server.on("error", (err) =>
|
|
44
|
-
this.h2Server.on("error", (err) =>
|
|
45
|
-
|
|
44
|
+
this.h1Server.on("error", (err) => log.error(`[LIOP-Gateway] H1 Server Error: ${err.message}`));
|
|
45
|
+
this.h2Server.on("error", (err) => log.error(`[LIOP-Gateway] H2 Server Error: ${err.message}`));
|
|
46
|
+
log.info("[LIOP-Gateway] Hybrid adapter initialized.");
|
|
46
47
|
}
|
|
47
48
|
setupH2Routes() {
|
|
48
49
|
this.h2Server.on("stream", (stream, headers) => {
|
|
@@ -62,6 +63,26 @@ export class LiopHybridGateway {
|
|
|
62
63
|
const method = req.method;
|
|
63
64
|
if (method === "GET" &&
|
|
64
65
|
(url === "/" || url === "/mcp" || url === "/health")) {
|
|
66
|
+
if (url === "/health" &&
|
|
67
|
+
req.headers.accept?.includes("application/json")) {
|
|
68
|
+
const meshInfo = this.meshNode
|
|
69
|
+
? {
|
|
70
|
+
peerId: this.meshNode.getPeerId()?.toString() || "",
|
|
71
|
+
multiaddrs: this.meshNode
|
|
72
|
+
.getMultiaddrs()
|
|
73
|
+
.map((m) => m.toString()),
|
|
74
|
+
}
|
|
75
|
+
: null;
|
|
76
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
77
|
+
res.end(JSON.stringify({
|
|
78
|
+
status: "healthy",
|
|
79
|
+
node: this.liopServer.getServerInfo(),
|
|
80
|
+
mesh: meshInfo,
|
|
81
|
+
tools: this.liopServer.listTools().map((t) => t.name),
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
}));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
65
86
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
66
87
|
res.end(`
|
|
67
88
|
<body style="background:#0f172a;color:#f8fafc;font-family:sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
@@ -88,7 +109,7 @@ export class LiopHybridGateway {
|
|
|
88
109
|
res.end(JSON.stringify(response));
|
|
89
110
|
}
|
|
90
111
|
catch (e) {
|
|
91
|
-
|
|
112
|
+
log.info(`[LIOP-Gateway] Error processing JSON-RPC payload: ${e.message}`);
|
|
92
113
|
res.writeHead(400);
|
|
93
114
|
res.end(JSON.stringify({
|
|
94
115
|
jsonrpc: "2.0",
|
|
@@ -108,7 +129,7 @@ export class LiopHybridGateway {
|
|
|
108
129
|
// biome-ignore lint/suspicious/noExplicitAny: Standard gRPC stream data is Buffer
|
|
109
130
|
const data = chunk;
|
|
110
131
|
if (data)
|
|
111
|
-
|
|
132
|
+
log.info(`[LIOP-Gateway] Native gRPC Proxy passing ${data.length} bytes`);
|
|
112
133
|
});
|
|
113
134
|
stream.respond({ ":status": 200, "content-type": "application/grpc" });
|
|
114
135
|
stream.end();
|
|
@@ -135,31 +156,32 @@ export class LiopHybridGateway {
|
|
|
135
156
|
}
|
|
136
157
|
});
|
|
137
158
|
}
|
|
138
|
-
async listen(port, host = "
|
|
159
|
+
async listen(port, host = "0.0.0.0") {
|
|
139
160
|
if (this.meshNode) {
|
|
140
161
|
await this.meshNode.start();
|
|
141
162
|
// Announce all local tools to the DHT
|
|
142
163
|
const tools = this.liopServer.listTools();
|
|
143
164
|
for (const tool of tools) {
|
|
144
165
|
await this.meshNode.announceCapability(tool.name);
|
|
145
|
-
|
|
166
|
+
log.info(`[LIOP-Gateway] 📡 Announced local tool to Mesh: ${tool.name}`);
|
|
146
167
|
}
|
|
147
168
|
}
|
|
148
169
|
return new Promise((resolve, reject) => {
|
|
149
170
|
this.netServer.on("error", (err) => {
|
|
150
171
|
if (err.code === "EADDRINUSE") {
|
|
151
|
-
|
|
172
|
+
log.info(`[LIOP-Gateway] FATAL: Port ${port} is already in use by another process.`);
|
|
152
173
|
}
|
|
153
174
|
else {
|
|
154
|
-
|
|
175
|
+
log.error(`[LIOP-Gateway] Binding Error: ${err.message}`);
|
|
155
176
|
}
|
|
156
177
|
reject(err);
|
|
157
178
|
});
|
|
158
179
|
this.netServer.listen(port, host, () => {
|
|
159
180
|
const addr = this.netServer.address();
|
|
160
181
|
const actualHost = typeof addr === "string" ? addr : addr?.address || host;
|
|
161
|
-
|
|
162
|
-
|
|
182
|
+
const assignedPort = typeof addr === "string" ? port : addr?.port || port;
|
|
183
|
+
log.info(`[LIOP-Gateway] ✅ Transformer Mesh Gateway READY and listening on ${actualHost}:${assignedPort}`);
|
|
184
|
+
resolve(assignedPort);
|
|
163
185
|
});
|
|
164
186
|
});
|
|
165
187
|
}
|
|
@@ -171,4 +193,7 @@ export class LiopHybridGateway {
|
|
|
171
193
|
this.h2Server.close();
|
|
172
194
|
this.h1Server.close();
|
|
173
195
|
}
|
|
196
|
+
getRouter() {
|
|
197
|
+
return this.router;
|
|
198
|
+
}
|
|
174
199
|
}
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { LiopVerifier } from "../crypto/verifier.js";
|
|
1
2
|
import type { MeshNode } from "../mesh/index.js";
|
|
2
3
|
import type { LiopServer } from "../server/index.js";
|
|
4
|
+
import type { McpRequest, McpResponse } from "../types.js";
|
|
3
5
|
/**
|
|
4
6
|
* LIOP MCP Router
|
|
5
7
|
*
|
|
@@ -18,21 +20,35 @@ export declare class LiopMcpRouter {
|
|
|
18
20
|
/** Guards against concurrent discovery storms */
|
|
19
21
|
private currentDiscovery;
|
|
20
22
|
/** Verifier for Tier-0 integrity checks */
|
|
21
|
-
|
|
23
|
+
verifier: LiopVerifier;
|
|
22
24
|
/** Callback when new remote tools are discovered */
|
|
23
25
|
onToolsChanged?: () => void;
|
|
26
|
+
/** Circuit-breaker state for peers that repeatedly fail manifest queries. */
|
|
27
|
+
private manifestFailureState;
|
|
28
|
+
private static readonly MANIFEST_FAILURE_BASE_COOLDOWN_MS;
|
|
29
|
+
private static readonly MANIFEST_FAILURE_MAX_COOLDOWN_MS;
|
|
30
|
+
private static readonly MANIFEST_SKIP_LOG_THROTTLE_MS;
|
|
24
31
|
constructor(liopServer: LiopServer, meshNode?: MeshNode | null, defaultRpcPort?: number);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
private shouldSkipManifestQuery;
|
|
33
|
+
private recordManifestQuerySuccess;
|
|
34
|
+
private recordManifestQueryFailure;
|
|
35
|
+
dispatch(request: McpRequest): Promise<McpResponse | null>;
|
|
36
|
+
/**
|
|
37
|
+
* MCP clients often send notifications/initialized then immediately tools/list.
|
|
38
|
+
* Start manifest discovery without blocking the notification handler.
|
|
39
|
+
*/
|
|
40
|
+
private kickDiscoveryAfterInitialized;
|
|
30
41
|
/**
|
|
31
42
|
* Discovers and caches manifests from all remote LIOP providers in the mesh.
|
|
32
43
|
* Uses Kademlia DHT to find "liop:manifest" providers, then opens
|
|
33
44
|
* /liop/manifest/1.0.0 protocol streams to retrieve their full metadata.
|
|
34
45
|
*/
|
|
35
46
|
refreshManifestCache(silent?: boolean): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Returns the current manifest cache size for external telemetry.
|
|
49
|
+
* Used by the adaptive polling system to detect topology stabilization.
|
|
50
|
+
*/
|
|
51
|
+
getCacheSize(): number;
|
|
36
52
|
/**
|
|
37
53
|
* Returns all remote tools discovered via the manifest protocol.
|
|
38
54
|
*/
|
|
@@ -42,10 +58,10 @@ export declare class LiopMcpRouter {
|
|
|
42
58
|
*/
|
|
43
59
|
private getRemoteResources;
|
|
44
60
|
/**
|
|
45
|
-
* Resolves the gRPC target (host:port) for a given tool name
|
|
46
|
-
* by searching the manifest cache.
|
|
61
|
+
* Resolves the gRPC target (host:port) AND the peerId for a given tool name
|
|
62
|
+
* by searching the manifest cache. Supports exact names and suffixed names.
|
|
47
63
|
*/
|
|
48
|
-
private
|
|
64
|
+
private resolveManifestTarget;
|
|
49
65
|
private transcodeMcpToLiop;
|
|
50
66
|
private routeToRemoteProvider;
|
|
51
67
|
private performTranscoding;
|