@nekzus/liop 1.2.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/dist/bin/agent.d.ts +2 -0
- package/dist/bin/agent.js +307 -0
- package/dist/bridge/index.d.ts +37 -0
- package/dist/bridge/index.js +249 -0
- package/dist/bridge/stream.d.ts +62 -0
- package/dist/bridge/stream.js +202 -0
- package/dist/client/index.d.ts +60 -0
- package/dist/client/index.js +275 -0
- package/dist/crypto/logic-image-id.d.ts +3 -0
- package/dist/crypto/logic-image-id.js +27 -0
- package/dist/crypto/verifier.d.ts +29 -0
- package/dist/crypto/verifier.js +96 -0
- 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/gateway/hybrid.d.ts +23 -0
- package/dist/gateway/hybrid.js +199 -0
- package/dist/gateway/router.d.ts +69 -0
- package/dist/gateway/router.js +1036 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/mesh/index.d.ts +1 -0
- package/dist/mesh/index.js +1 -0
- package/dist/mesh/node.d.ts +129 -0
- package/dist/mesh/node.js +853 -0
- package/dist/prompts/adapters.d.ts +16 -0
- package/dist/prompts/adapters.js +55 -0
- package/dist/protocol/liop_core.proto +44 -0
- package/dist/rpc/client.d.ts +22 -0
- package/dist/rpc/client.js +40 -0
- package/dist/rpc/codec/lpm.d.ts +20 -0
- package/dist/rpc/codec/lpm.js +36 -0
- package/dist/rpc/crypto/aes.d.ts +22 -0
- package/dist/rpc/crypto/aes.js +47 -0
- package/dist/rpc/crypto/kyber.d.ts +27 -0
- package/dist/rpc/crypto/kyber.js +70 -0
- package/dist/rpc/proto.d.ts +2 -0
- package/dist/rpc/proto.js +33 -0
- package/dist/rpc/server.d.ts +13 -0
- package/dist/rpc/server.js +50 -0
- package/dist/rpc/tls.d.ts +26 -0
- package/dist/rpc/tls.js +54 -0
- package/dist/rpc/types.d.ts +28 -0
- package/dist/rpc/types.js +5 -0
- package/dist/sandbox/guardian.d.ts +18 -0
- package/dist/sandbox/guardian.js +35 -0
- package/dist/sandbox/wasi.d.ts +36 -0
- package/dist/sandbox/wasi.js +179 -0
- package/dist/security/guardian.d.ts +22 -0
- package/dist/security/guardian.js +52 -0
- package/dist/security/zk.d.ts +37 -0
- package/dist/security/zk.js +66 -0
- package/dist/server/index.d.ts +184 -0
- package/dist/server/index.js +933 -0
- package/dist/server/pii.d.ts +40 -0
- package/dist/server/pii.js +266 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.js +26 -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 +17 -0
- package/dist/workers/logic-execution.js +121 -0
- package/dist/workers/zk-verifier.d.ts +20 -0
- package/dist/workers/zk-verifier.js +84 -0
- package/package.json +147 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
6
|
+
import { LiopMcpRouter } from "../gateway/router.js";
|
|
7
|
+
import { MeshNode } from "../mesh/index.js";
|
|
8
|
+
import { LiopServer } from "../server/index.js";
|
|
9
|
+
import { log } from "../utils/logger.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolves a full libp2p multiaddr (with PeerID) from a LIOP node's
|
|
12
|
+
* HTTP health endpoint. This enables zero-config bootstrap — users
|
|
13
|
+
* only need to provide a URL, not a cryptographic PeerID.
|
|
14
|
+
*
|
|
15
|
+
* @param url - HTTP URL of a LIOP node's health endpoint (e.g. "http://host:3000")
|
|
16
|
+
* @returns Full multiaddr string with PeerID, or null if resolution fails
|
|
17
|
+
*/
|
|
18
|
+
async function resolveBootstrapFromUrl(url) {
|
|
19
|
+
try {
|
|
20
|
+
const healthUrl = url.endsWith("/health") ? url : `${url}/health`;
|
|
21
|
+
const response = await fetch(healthUrl, {
|
|
22
|
+
headers: { Accept: "application/json" },
|
|
23
|
+
signal: AbortSignal.timeout(10000), // Increased to 10s
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok)
|
|
26
|
+
return null;
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
if (!data.mesh?.multiaddrs?.length || !data.mesh?.peerId)
|
|
29
|
+
return null;
|
|
30
|
+
// Find TCP multiaddr (prefer non-websocket for stability)
|
|
31
|
+
const tcpAddr = data.mesh.multiaddrs.find((a) => a.includes("/tcp/") &&
|
|
32
|
+
!a.includes("/ws") &&
|
|
33
|
+
!a.includes("/ip4/127.0.0.1/"));
|
|
34
|
+
if (!tcpAddr)
|
|
35
|
+
return null;
|
|
36
|
+
// Rewrite internal Docker IP using industrial mapper if available
|
|
37
|
+
let resolved = industrialAddressMapper(tcpAddr);
|
|
38
|
+
if (!resolved || resolved === tcpAddr) {
|
|
39
|
+
const urlHost = new URL(url).hostname;
|
|
40
|
+
resolved = tcpAddr.replace(/\/ip4\/[^/]+/, `/ip4/${urlHost}`);
|
|
41
|
+
}
|
|
42
|
+
if (!resolved)
|
|
43
|
+
return null;
|
|
44
|
+
resolved += resolved.includes("/p2p/") ? "" : `/p2p/${data.mesh.peerId}`;
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Normalizes a bootstrap multiaddr string.
|
|
53
|
+
* If the address contains a Docker bridge IP (172.16-31.x.x) or Loopback (127.0.0.1),
|
|
54
|
+
* rewrites it to the host accessible via LIOP_NEXUS_URL (e.g. WSL2 IP).
|
|
55
|
+
* This is critical when WSL2 mirror-mode networking is broken.
|
|
56
|
+
*/
|
|
57
|
+
function normalizeBootstrap(addr) {
|
|
58
|
+
const trimmed = addr.trim();
|
|
59
|
+
// Remap Docker bridge IPs and ANY external physical IPs to 127.0.0.1
|
|
60
|
+
// because Test-NetConnection confirmed 127.0.0.1 is the only reliable path to Docker ports.
|
|
61
|
+
const dockerIpRegex = /\/ip4\/172\.(1[6-9]|2[0-9]|3[0-1])\.[0-9]{1,3}\.[0-9]{1,3}/;
|
|
62
|
+
const loopbackRegex = /\/ip4\/127\.0\.0\.1/;
|
|
63
|
+
const physicalIpRegex = /\/ip4\/192\.168\.[0-9]{1,3}\.[0-9]{1,3}/;
|
|
64
|
+
if (dockerIpRegex.test(trimmed) ||
|
|
65
|
+
loopbackRegex.test(trimmed) ||
|
|
66
|
+
physicalIpRegex.test(trimmed)) {
|
|
67
|
+
const targetIp = "127.0.0.1";
|
|
68
|
+
const normalized = trimmed
|
|
69
|
+
.replace(dockerIpRegex, `/ip4/${targetIp}`)
|
|
70
|
+
.replace(loopbackRegex, `/ip4/${targetIp}`)
|
|
71
|
+
.replace(physicalIpRegex, `/ip4/${targetIp}`);
|
|
72
|
+
if (normalized !== trimmed) {
|
|
73
|
+
log.info(`[LIOP-Agent] 🔄 Local Routing Hack → Forced 127.0.0.1: ${normalized}`);
|
|
74
|
+
}
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
return trimmed;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* industrialAddressMapper
|
|
81
|
+
*
|
|
82
|
+
* Mapea IPs internas de Docker a puertos industriales mapeados en el Host.
|
|
83
|
+
* Nexus (172.20.0.10) -> 13001
|
|
84
|
+
* Vault (172.20.0.11) -> 13003
|
|
85
|
+
* Bank (172.20.0.12) -> 13004
|
|
86
|
+
* Oracle(172.20.0.13) -> 13005
|
|
87
|
+
*/
|
|
88
|
+
function industrialAddressMapper(addr) {
|
|
89
|
+
if (addr.includes("/ip4/172.20.0.10"))
|
|
90
|
+
return addr.replace(/\/ip4\/172\.20\.0\.10\/tcp\/[0-9]+/, "/ip4/127.0.0.1/tcp/13001");
|
|
91
|
+
if (addr.includes("/ip4/172.20.0.11"))
|
|
92
|
+
return addr.replace(/\/ip4\/172\.20\.0\.11\/tcp\/[0-9]+/, "/ip4/127.0.0.1/tcp/13003");
|
|
93
|
+
if (addr.includes("/ip4/172.20.0.12"))
|
|
94
|
+
return addr.replace(/\/ip4\/172\.20\.0\.12\/tcp\/[0-9]+/, "/ip4/127.0.0.1/tcp/13004");
|
|
95
|
+
if (addr.includes("/ip4/172.20.0.13"))
|
|
96
|
+
return addr.replace(/\/ip4\/172\.20\.0\.13\/tcp\/[0-9]+/, "/ip4/127.0.0.1/tcp/13005");
|
|
97
|
+
// Drop container-internal loopbacks to prevent the Host Agent from dialing itself or conflicting ports
|
|
98
|
+
if (addr.includes("/ip4/127.0.0.1/tcp/4000") ||
|
|
99
|
+
addr.includes("/ip4/127.0.0.1/tcp/3000")) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return addr;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* LIOP Agent (Zero-Config CLI)
|
|
106
|
+
*
|
|
107
|
+
* Secure Logic-on-Origin gateway for Claude Desktop.
|
|
108
|
+
* Communicates via STDIO / JSON-RPC.
|
|
109
|
+
*
|
|
110
|
+
* All tool discovery is DYNAMIC via the /liop/manifest/1.0.0 protocol.
|
|
111
|
+
* No hardcoded tools, PeerIDs, or port mappings.
|
|
112
|
+
*/
|
|
113
|
+
async function main() {
|
|
114
|
+
const buildTime = new Date().toISOString();
|
|
115
|
+
log.info(`[LIOP-Agent] 🚀 Version 1.2.0-alpha.9 | Build: ${buildTime}`);
|
|
116
|
+
const liopDir = path.join(os.homedir(), ".liop");
|
|
117
|
+
const identityPath = path.join(liopDir, "identity.json");
|
|
118
|
+
if (!fs.existsSync(liopDir)) {
|
|
119
|
+
fs.mkdirSync(liopDir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
// 1. Determine Bootstrap Nodes (Zero-Config Discovery)
|
|
122
|
+
let bootstrapNodes = [];
|
|
123
|
+
// Command line arguments take precedence
|
|
124
|
+
const args = process.argv.slice(2);
|
|
125
|
+
if (args.length > 0) {
|
|
126
|
+
bootstrapNodes = args.filter((a) => a.startsWith("/"));
|
|
127
|
+
}
|
|
128
|
+
// Priority 1: Physical Beacons (Industrial Pattern) - DETERMINISTIC & INSTANT
|
|
129
|
+
if (bootstrapNodes.length === 0) {
|
|
130
|
+
const searchDirs = [];
|
|
131
|
+
// Priority 1.1: Explicit file from environment variable
|
|
132
|
+
if (process.env.LIOP_BOOTSTRAP_FILE) {
|
|
133
|
+
const filePath = path.resolve(process.env.LIOP_BOOTSTRAP_FILE);
|
|
134
|
+
if (fs.existsSync(filePath)) {
|
|
135
|
+
const addr = fs.readFileSync(filePath, "utf8").trim();
|
|
136
|
+
if (addr)
|
|
137
|
+
bootstrapNodes.push(normalizeBootstrap(addr));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Priority 1.2: Traditional locations (Scan for all *.multiaddr)
|
|
141
|
+
searchDirs.push(process.cwd(), path.join(process.cwd(), "tests/infra/nexus-data"), liopDir, path.join(path
|
|
142
|
+
.dirname(new URL(import.meta.url).pathname)
|
|
143
|
+
.replace(/^\/([A-Z]:)/, "$1"), "../../tests/infra/nexus-data"));
|
|
144
|
+
for (const dir of searchDirs) {
|
|
145
|
+
try {
|
|
146
|
+
if (fs.existsSync(dir)) {
|
|
147
|
+
const files = fs.readdirSync(dir);
|
|
148
|
+
const multiaddrFiles = files.filter((f) => f.endsWith(".multiaddr"));
|
|
149
|
+
for (const file of multiaddrFiles) {
|
|
150
|
+
const filePath = path.join(dir, file);
|
|
151
|
+
const addr = fs.readFileSync(filePath, "utf8").trim();
|
|
152
|
+
if (addr) {
|
|
153
|
+
const normalized = normalizeBootstrap(addr);
|
|
154
|
+
if (!bootstrapNodes.includes(normalized)) {
|
|
155
|
+
bootstrapNodes.push(normalized);
|
|
156
|
+
log.info(`[LIOP-Agent] ✅ Loaded beacon: ${file} from ${dir}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// If we found any beacons in this directory, we consider discovery successful for this layer
|
|
161
|
+
if (bootstrapNodes.length > 0)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (_e) {
|
|
166
|
+
/* ignore */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Priority 2: Auto-Discovery via NEXUS URL (Aggressive Parallel Discovery)
|
|
171
|
+
if (process.env.LIOP_NEXUS_URL) {
|
|
172
|
+
const nexusUrl = process.env.LIOP_NEXUS_URL;
|
|
173
|
+
log.info(`[LIOP-Agent] 🌐 Running parallel discovery from: ${nexusUrl} (Sources Found: ${bootstrapNodes.length})`);
|
|
174
|
+
const resolved = await resolveBootstrapFromUrl(nexusUrl);
|
|
175
|
+
if (resolved) {
|
|
176
|
+
const normalized = normalizeBootstrap(resolved);
|
|
177
|
+
if (!bootstrapNodes.includes(normalized)) {
|
|
178
|
+
bootstrapNodes.push(normalized);
|
|
179
|
+
log.info(`[LIOP-Agent] ✅ Added bootstrap from URL discovery: ${normalized}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Priority 3: Environment variable (direct multiaddr)
|
|
184
|
+
if (bootstrapNodes.length === 0 && process.env.LIOP_BOOTSTRAP) {
|
|
185
|
+
bootstrapNodes.push(process.env.LIOP_BOOTSTRAP.trim());
|
|
186
|
+
}
|
|
187
|
+
// Final fallback: local Nexus bootstrap for demo environments.
|
|
188
|
+
// Avoid injecting stale static peer IDs when discovery already found valid peers.
|
|
189
|
+
if (bootstrapNodes.length === 0) {
|
|
190
|
+
bootstrapNodes.push("/ip4/127.0.0.1/tcp/13001/p2p/12D3KooWD8FUFdnLQzzLFNdicsaTknM5cpD7os9sK9NWVSVABJMD");
|
|
191
|
+
}
|
|
192
|
+
// Sanitize/validate all candidate multiaddrs so malformed PeerIDs don't crash startup.
|
|
193
|
+
bootstrapNodes = bootstrapNodes.filter((addr) => {
|
|
194
|
+
try {
|
|
195
|
+
multiaddr(addr);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
log.warn(`[LIOP-Agent] Ignoring invalid bootstrap multiaddr: ${addr}`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
// If no bootstrap nodes found, the agent operates in standalone mode.
|
|
204
|
+
// It will only serve local tools until peers are discovered.
|
|
205
|
+
if (bootstrapNodes.length === 0) {
|
|
206
|
+
log.info("[LIOP-Agent] No bootstrap nodes configured. Operating in standalone mode.");
|
|
207
|
+
log.info("[LIOP-Agent] Pass a multiaddr as argument or create 'nexus.multiaddr' file.");
|
|
208
|
+
}
|
|
209
|
+
// Initialize local server node (lightweight, no tools registered locally)
|
|
210
|
+
const liopServer = new LiopServer({
|
|
211
|
+
name: "@nekzus/liop-agent",
|
|
212
|
+
version: "1.0.0",
|
|
213
|
+
});
|
|
214
|
+
// Enable Zero-Shot Autonomy (Industrial Prompt Injection)
|
|
215
|
+
liopServer.enableZeroShotAutonomy();
|
|
216
|
+
// 2. Mesh Node Configuration
|
|
217
|
+
const meshNode = new MeshNode({
|
|
218
|
+
identityPath: identityPath,
|
|
219
|
+
bootstrapNodes: bootstrapNodes,
|
|
220
|
+
addressMapper: industrialAddressMapper,
|
|
221
|
+
});
|
|
222
|
+
// Start P2P Mesh
|
|
223
|
+
await meshNode.start();
|
|
224
|
+
// 3. Initialize the Dynamic Router
|
|
225
|
+
// No hardcoded tools — all discovery happens via liop:manifest protocol
|
|
226
|
+
const router = new LiopMcpRouter(liopServer, meshNode);
|
|
227
|
+
// Proactive Notification to Claude Desktop when tools/resources are discovered dynamically
|
|
228
|
+
router.onToolsChanged = () => {
|
|
229
|
+
process.stdout.write(`{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}\n`);
|
|
230
|
+
process.stdout.write(`{"jsonrpc":"2.0","method":"notifications/resources/list_changed"}\n`);
|
|
231
|
+
};
|
|
232
|
+
// Initial warming period (2s) then Adaptive Background Discovery
|
|
233
|
+
// Polls DHT for new nodes and triggers onToolsChanged when topology shifts.
|
|
234
|
+
// Uses exponential backoff to reduce polling load on stable meshes.
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
// biome-ignore lint/suspicious/noExplicitAny: access internal for telemetry
|
|
237
|
+
const rtSize = meshNode.getRoutingTableSize?.() || 0;
|
|
238
|
+
log.info(`[LIOP-Agent] Warm-up complete. Routing Table size: ${rtSize}`);
|
|
239
|
+
router.refreshManifestCache(true).catch(() => { });
|
|
240
|
+
}, 2000);
|
|
241
|
+
const POLL_BASE_MS = 10_000;
|
|
242
|
+
const POLL_MAX_MS = 120_000;
|
|
243
|
+
let pollIntervalMs = POLL_BASE_MS;
|
|
244
|
+
const scheduleAdaptivePoll = () => {
|
|
245
|
+
setTimeout(async () => {
|
|
246
|
+
const prevSize = router.getCacheSize();
|
|
247
|
+
await router.refreshManifestCache(true).catch(() => { });
|
|
248
|
+
const newSize = router.getCacheSize();
|
|
249
|
+
if (newSize !== prevSize) {
|
|
250
|
+
// Topology changed — reset to aggressive polling
|
|
251
|
+
pollIntervalMs = POLL_BASE_MS;
|
|
252
|
+
log.info(`[LIOP-Agent] Topology change detected (${prevSize} → ${newSize}). Resetting poll to ${POLL_BASE_MS / 1000}s.`);
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Stable — relax polling interval (factor 1.5)
|
|
256
|
+
pollIntervalMs = Math.min(Math.round(pollIntervalMs * 1.5), POLL_MAX_MS);
|
|
257
|
+
}
|
|
258
|
+
scheduleAdaptivePoll();
|
|
259
|
+
}, pollIntervalMs);
|
|
260
|
+
};
|
|
261
|
+
scheduleAdaptivePoll();
|
|
262
|
+
// 4. STDIO Transport — Buffered Line Reader
|
|
263
|
+
// Uses readline to guarantee complete JSON-RPC messages before parsing.
|
|
264
|
+
// Raw stdin.on("data") can fragment large payloads across multiple chunks.
|
|
265
|
+
const readline = await import("node:readline");
|
|
266
|
+
const rl = readline.createInterface({
|
|
267
|
+
input: process.stdin,
|
|
268
|
+
terminal: false,
|
|
269
|
+
});
|
|
270
|
+
process.stdout.on("error", (err) => {
|
|
271
|
+
if (err.code === "EPIPE") {
|
|
272
|
+
process.exit(0); // Graceful exit when Claude Desktop disconnects
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
rl.on("line", async (line) => {
|
|
276
|
+
const trimmed = line.trim();
|
|
277
|
+
if (!trimmed)
|
|
278
|
+
return;
|
|
279
|
+
try {
|
|
280
|
+
const request = JSON.parse(trimmed);
|
|
281
|
+
if (request.method) {
|
|
282
|
+
const response = await router.dispatch(request);
|
|
283
|
+
if (response) {
|
|
284
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (_err) {
|
|
289
|
+
// Silent catch for binary noise or malformed lines
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
rl.on("close", () => {
|
|
293
|
+
process.exit(0);
|
|
294
|
+
});
|
|
295
|
+
// Status directed only to stderr
|
|
296
|
+
log.info(`[LIOP-Agent] Guarding Claude Desktop via STDIO.`);
|
|
297
|
+
log.info(`[LIOP-Agent] P2P Mesh: Joined (${bootstrapNodes.length} bootstraps)`);
|
|
298
|
+
log.info("[LIOP-Agent] Tool discovery: Dynamic via /liop/manifest/1.0.0");
|
|
299
|
+
process.on("SIGINT", async () => {
|
|
300
|
+
await meshNode.stop();
|
|
301
|
+
process.exit(0);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
main().catch((err) => {
|
|
305
|
+
log.error(`[LIOP-Agent] Fatal Error: ${err.message}`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { LiopServerOptions } from "../server/index.js";
|
|
3
|
+
import { LiopServer } from "../server/index.js";
|
|
4
|
+
export interface LiopBridgeOptions {
|
|
5
|
+
publishToMesh?: boolean;
|
|
6
|
+
meshIdentity?: string;
|
|
7
|
+
serverInfo?: {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
security?: LiopServerOptions["security"];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* LIOP MCP Bridge
|
|
15
|
+
* A bi-directional bridge that allows legacy MCP servers to join the LIOP mesh,
|
|
16
|
+
* or exposes a LIOP server as an MCP-compatible stdio process for tools like Claude Desktop.
|
|
17
|
+
*/
|
|
18
|
+
export declare class LiopMcpBridge {
|
|
19
|
+
private options;
|
|
20
|
+
private liopServer;
|
|
21
|
+
private legacyMcpServer;
|
|
22
|
+
constructor(source: LiopServer | McpServer, options?: LiopBridgeOptions);
|
|
23
|
+
/**
|
|
24
|
+
* Handles an incoming standard MCP JSON-RPC 2.0 payload.
|
|
25
|
+
* Pipes it to the underlying server (LIOP or Legacy MCP).
|
|
26
|
+
*/
|
|
27
|
+
handleJsonRpcRequest(payload: Record<string, unknown>): Promise<unknown>;
|
|
28
|
+
private handleLiopToMcp;
|
|
29
|
+
private successResponse;
|
|
30
|
+
private errorResponse;
|
|
31
|
+
private verifyZkReceipt;
|
|
32
|
+
/**
|
|
33
|
+
* Connects the bridge via stdio or Mesh depending on mode.
|
|
34
|
+
*/
|
|
35
|
+
connect(): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export * from "./stream.js";
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { LiopServer } from "../server/index.js";
|
|
3
|
+
import { log } from "../utils/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* LIOP MCP Bridge
|
|
6
|
+
* A bi-directional bridge that allows legacy MCP servers to join the LIOP mesh,
|
|
7
|
+
* or exposes a LIOP server as an MCP-compatible stdio process for tools like Claude Desktop.
|
|
8
|
+
*/
|
|
9
|
+
export class LiopMcpBridge {
|
|
10
|
+
options;
|
|
11
|
+
liopServer = null;
|
|
12
|
+
legacyMcpServer = null;
|
|
13
|
+
constructor(source, options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
// Determine mode: Exposing LIOP to MCP (Claude) or Wrapping MCP to LIOP (Mesh)
|
|
16
|
+
if (source instanceof LiopServer) {
|
|
17
|
+
this.liopServer = source;
|
|
18
|
+
log.info("[LIOP-Bridge] Mode: EXPOSE (LIOP -> MCP Stdio)");
|
|
19
|
+
}
|
|
20
|
+
else if (source instanceof McpServer) {
|
|
21
|
+
this.legacyMcpServer = source;
|
|
22
|
+
log.info("[LIOP-Bridge] Mode: WRAP (Legacy MCP -> LIOP Mesh)");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Handles an incoming standard MCP JSON-RPC 2.0 payload.
|
|
27
|
+
* Pipes it to the underlying server (LIOP or Legacy MCP).
|
|
28
|
+
*/
|
|
29
|
+
async handleJsonRpcRequest(payload) {
|
|
30
|
+
const id = payload.id;
|
|
31
|
+
const method = payload.method;
|
|
32
|
+
const params = payload.params;
|
|
33
|
+
if (payload.jsonrpc !== "2.0") {
|
|
34
|
+
return this.errorResponse(id, -32600, "Invalid Request");
|
|
35
|
+
}
|
|
36
|
+
// Mode: EXPOSE (Standard behavior used by Claude Desktop)
|
|
37
|
+
if (this.liopServer) {
|
|
38
|
+
return this.handleLiopToMcp(id, method, params);
|
|
39
|
+
}
|
|
40
|
+
// Mode: WRAP (Redirecting via internal LiopServer after connect())
|
|
41
|
+
if (this.legacyMcpServer && this.liopServer) {
|
|
42
|
+
return this.handleLiopToMcp(id, method, params);
|
|
43
|
+
}
|
|
44
|
+
return this.errorResponse(id, -32601, "Bridge source not configured");
|
|
45
|
+
}
|
|
46
|
+
async handleLiopToMcp(id, method, params) {
|
|
47
|
+
if (!this.liopServer)
|
|
48
|
+
return null;
|
|
49
|
+
if (method === "initialize") {
|
|
50
|
+
return this.successResponse(id, {
|
|
51
|
+
protocolVersion: "2025-11-25",
|
|
52
|
+
capabilities: {
|
|
53
|
+
prompts: {},
|
|
54
|
+
resources: {},
|
|
55
|
+
tools: {},
|
|
56
|
+
},
|
|
57
|
+
serverInfo: this.liopServer.getServerInfo(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (method === "notifications/initialized")
|
|
61
|
+
return undefined;
|
|
62
|
+
if (method === "ping")
|
|
63
|
+
return this.successResponse(id, {});
|
|
64
|
+
if (method === "tools/list") {
|
|
65
|
+
const tools = this.liopServer.listTools();
|
|
66
|
+
return this.successResponse(id, { tools });
|
|
67
|
+
}
|
|
68
|
+
if (method === "resources/list") {
|
|
69
|
+
const resources = this.liopServer.listResources();
|
|
70
|
+
return this.successResponse(id, { resources });
|
|
71
|
+
}
|
|
72
|
+
if (method === "prompts/list") {
|
|
73
|
+
const prompts = this.liopServer.listPrompts();
|
|
74
|
+
return this.successResponse(id, { prompts });
|
|
75
|
+
}
|
|
76
|
+
if (method === "prompts/get") {
|
|
77
|
+
if (!params?.name) {
|
|
78
|
+
return this.errorResponse(id, -32602, "Missing prompt name");
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const result = await this.liopServer.getPrompt({
|
|
82
|
+
name: params.name,
|
|
83
|
+
arguments: params.arguments,
|
|
84
|
+
});
|
|
85
|
+
return this.successResponse(id, result);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return this.errorResponse(id, -32000, err.message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (method === "resources/read") {
|
|
92
|
+
if (!params?.uri) {
|
|
93
|
+
return this.errorResponse(id, -32602, "Missing resource URI");
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const result = await this.liopServer.readResource(params.uri);
|
|
97
|
+
return this.successResponse(id, result);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return this.errorResponse(id, -32000, err.message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (method === "tools/call") {
|
|
104
|
+
if (!params?.name) {
|
|
105
|
+
return this.errorResponse(id, -32602, "Missing tool name");
|
|
106
|
+
}
|
|
107
|
+
const request = {
|
|
108
|
+
name: params.name,
|
|
109
|
+
arguments: params.arguments || {},
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
const result = await this.liopServer.callTool(request);
|
|
113
|
+
const isVerified = await this.verifyZkReceipt(request, result);
|
|
114
|
+
if (!isVerified) {
|
|
115
|
+
return this.successResponse(id, {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: "ALERT [LIOP ZERO-TRUST SHIELD] ZK Verification Failed. The mathematical ImageID does not match the original payload.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
isError: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return this.successResponse(id, result);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
return this.errorResponse(id, -32000, err.message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return this.errorResponse(id, -32601, "Method not found");
|
|
132
|
+
}
|
|
133
|
+
successResponse(id, result) {
|
|
134
|
+
return { jsonrpc: "2.0", id, result };
|
|
135
|
+
}
|
|
136
|
+
errorResponse(id, code, message) {
|
|
137
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
138
|
+
}
|
|
139
|
+
async verifyZkReceipt(request, result) {
|
|
140
|
+
if (!request.arguments?.payload ||
|
|
141
|
+
typeof request.arguments.payload !== "string") {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const payload = request.arguments.payload;
|
|
146
|
+
const contentText = result.content[0]?.text;
|
|
147
|
+
if (contentText && typeof contentText === "string") {
|
|
148
|
+
try {
|
|
149
|
+
const data = JSON.parse(contentText);
|
|
150
|
+
if (data.image_id || data.zk_receipt) {
|
|
151
|
+
// 1. Instantiate the Industrial Verifier ( backed by Piscina Worker Pool )
|
|
152
|
+
const { LiopVerifier } = await import("../crypto/verifier.js");
|
|
153
|
+
const verifier = new LiopVerifier();
|
|
154
|
+
// 2. Delegate the heavy mathematical check (ZK Journal + Seal)
|
|
155
|
+
const isAuthentic = await verifier.verifyZkReceipt(Buffer.from(payload, "utf-8"), data.image_id, Buffer.from(data.zk_receipt || "", "base64"));
|
|
156
|
+
if (!isAuthentic) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
data.audit_status =
|
|
160
|
+
"VERIFIED: ZK-Receipt & ImageID Mathematically Verified by LiopMcpBridge";
|
|
161
|
+
result.content[0].text = JSON.stringify(data);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Output not JSON
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
log.info("[LIOP-Bridge] ZK-Verifier Failure:", e);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Connects the bridge via stdio or Mesh depending on mode.
|
|
177
|
+
*/
|
|
178
|
+
async connect() {
|
|
179
|
+
// In WRAP mode, we actually need to create a LiopServer and join the mesh
|
|
180
|
+
if (this.legacyMcpServer) {
|
|
181
|
+
const { LiopServer } = await import("../server/index.js");
|
|
182
|
+
this.liopServer = new LiopServer(this.options.serverInfo || {
|
|
183
|
+
name: "liop-bridge",
|
|
184
|
+
version: "1.0.0",
|
|
185
|
+
}, { security: this.options.security });
|
|
186
|
+
if (this.options.publishToMesh) {
|
|
187
|
+
await this.liopServer.connect();
|
|
188
|
+
// Automatically Bridge Legacy Capabilities to LIOP Mesh
|
|
189
|
+
// biome-ignore lint/suspicious/noExplicitAny: Internal legacy MCP properties are completely opaque and unexported
|
|
190
|
+
const legacy = this.legacyMcpServer;
|
|
191
|
+
// 1. Sync Tools
|
|
192
|
+
if (legacy._registeredTools) {
|
|
193
|
+
for (const [name, tool] of Object.entries(legacy._registeredTools)) {
|
|
194
|
+
// biome-ignore lint/suspicious/noExplicitAny: Opaque legacy structure
|
|
195
|
+
const t = tool;
|
|
196
|
+
this.liopServer.tool(name, t.description || "", t.inputSchema || {},
|
|
197
|
+
// biome-ignore lint/suspicious/noExplicitAny: Opaque legacy callback args
|
|
198
|
+
async (args) => {
|
|
199
|
+
return await t.handler(args);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 2. Sync Resources
|
|
204
|
+
if (legacy._registeredResources) {
|
|
205
|
+
for (const [uri, resource] of Object.entries(legacy._registeredResources)) {
|
|
206
|
+
// biome-ignore lint/suspicious/noExplicitAny: Opaque legacy structure
|
|
207
|
+
const r = resource;
|
|
208
|
+
this.liopServer.resource(r.name, uri, r.metadata?.description || "", r.metadata?.mimeType || "application/octet-stream", async () => {
|
|
209
|
+
const res = await r.readCallback(new URL(uri));
|
|
210
|
+
return res.contents[0].text;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// In EXPOSE mode, listen to stdio (Claude Desktop)
|
|
218
|
+
const readline = await import("node:readline");
|
|
219
|
+
const rl = readline.createInterface({
|
|
220
|
+
input: process.stdin,
|
|
221
|
+
output: process.stdout,
|
|
222
|
+
terminal: false,
|
|
223
|
+
});
|
|
224
|
+
const shutdown = async () => {
|
|
225
|
+
log.info("[LIOP-Bridge] Disconnecting session...");
|
|
226
|
+
if (this.liopServer)
|
|
227
|
+
await this.liopServer.close();
|
|
228
|
+
process.exit(0);
|
|
229
|
+
};
|
|
230
|
+
rl.on("close", shutdown);
|
|
231
|
+
process.on("SIGINT", shutdown);
|
|
232
|
+
process.on("SIGTERM", shutdown);
|
|
233
|
+
rl.on("line", async (line) => {
|
|
234
|
+
if (!line.trim())
|
|
235
|
+
return;
|
|
236
|
+
try {
|
|
237
|
+
const payload = JSON.parse(line);
|
|
238
|
+
const response = await this.handleJsonRpcRequest(payload);
|
|
239
|
+
if (response) {
|
|
240
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
log.error(`[LIOP-Bridge] Error: ${e.message}`);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
export * from "./stream.js";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LiopServer } from "../server/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for LiopStreamBridge.
|
|
4
|
+
*/
|
|
5
|
+
export interface LiopStreamBridgeOptions {
|
|
6
|
+
/** Port to listen on (default: 3000) */
|
|
7
|
+
port?: number;
|
|
8
|
+
/** Max concurrent sessions per IP (default: 5) */
|
|
9
|
+
maxSessionsPerIp?: number;
|
|
10
|
+
/** Session idle timeout in milliseconds (default: 30 min) */
|
|
11
|
+
sessionTimeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* LiopStreamBridge
|
|
15
|
+
*
|
|
16
|
+
* Exposes a LiopServer over a remote HTTP network using the industry-standard
|
|
17
|
+
* MCP Streamable HTTP Transport + Hono JS.
|
|
18
|
+
*
|
|
19
|
+
* Supports concurrent multi-client connections via per-session transport instances (Map pattern).
|
|
20
|
+
* External agents connect using only a URL + Bearer Token (Zero-Trust).
|
|
21
|
+
*
|
|
22
|
+
* Security hardening:
|
|
23
|
+
* - Zero-Trust Bearer Token enforcement
|
|
24
|
+
* - Per-IP rate limiting on session creation
|
|
25
|
+
* - Automatic eviction of idle sessions (TTL)
|
|
26
|
+
*/
|
|
27
|
+
export declare class LiopStreamBridge {
|
|
28
|
+
private options;
|
|
29
|
+
private app;
|
|
30
|
+
private httpServer;
|
|
31
|
+
private bridgeLogic;
|
|
32
|
+
private activeSessions;
|
|
33
|
+
private evictionTimer;
|
|
34
|
+
private maxSessionsPerIp;
|
|
35
|
+
private sessionTimeoutMs;
|
|
36
|
+
constructor(internalServer: LiopServer, options?: LiopStreamBridgeOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Creates a new per-session transport instance and wires it to the LIOPMcpBridge logic.
|
|
39
|
+
*/
|
|
40
|
+
private createSessionTransport;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the number of active sessions for a given IP.
|
|
43
|
+
*/
|
|
44
|
+
private countSessionsByIp;
|
|
45
|
+
/**
|
|
46
|
+
* Extracts client IP from the request (supports X-Forwarded-For for reverse proxies).
|
|
47
|
+
*/
|
|
48
|
+
private getClientIp;
|
|
49
|
+
/**
|
|
50
|
+
* Evicts sessions that have been idle longer than the configured timeout.
|
|
51
|
+
*/
|
|
52
|
+
private evictIdleSessions;
|
|
53
|
+
private setupRoutes;
|
|
54
|
+
/**
|
|
55
|
+
* Starts the LiopStreamBridge HTTP server and session eviction timer.
|
|
56
|
+
*/
|
|
57
|
+
start(port?: number): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Graceful shutdown — closes all active sessions, stops timers, and releases port.
|
|
60
|
+
*/
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
}
|