@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
package/README.md
CHANGED
|
@@ -36,9 +36,10 @@ This fundamentally solves the data privacy, bandwidth, and latency challenges of
|
|
|
36
36
|
| **MCP Drop-in Replacement** | `LiopServer` mirrors the Anthropic MCP `Server` API — tools, resources, and prompts with `Zod` schemas. |
|
|
37
37
|
| **Guardian AST** | Zero-time heuristic inspection blocks sandbox escapes (`require`, `fs`, `eval`, `fetch`, prototype pollution). |
|
|
38
38
|
| **WASI Sandbox** | JavaScript payloads execute inside V8 isolates with CPU fuel limits and no access to Node.js globals. |
|
|
39
|
-
| **PII Shield** | Multi-layer egress filter with
|
|
39
|
+
| **PII Shield** | Multi-layer egress filter with Regional Presets (Email, Credit Card with Luhn, IP, Phone, SSN, IBAN Mod-97, Passport MRZ) and custom keys. |
|
|
40
40
|
| **ZK-Receipts** | Cryptographic proof (SHA-256 + SHA-512 seal) that the returned result was computed honestly from the injected logic. |
|
|
41
41
|
| **Worker Pool** | Heavy computation (crypto, sandboxing) dispatched to OS threads via `piscina`, unblocking the V8 event loop. |
|
|
42
|
+
| **Cross-AI Adapters**| Zero-Shot system prompts automatically adapt instructions for Claude (XML-heavy) vs OpenAI/Gemini (JSON-schema). |
|
|
42
43
|
| **MCP Bridge** | `LiopMcpBridge` adapts any `LiopServer` to the JSON-RPC 2.0 / stdio protocol used by Claude Desktop, Cursor, etc. |
|
|
43
44
|
| **Post-Quantum Ready** | ML-KEM-768 (Kyber) handshake + AES-256-GCM symmetric encryption for transport-layer security. |
|
|
44
45
|
| **P2P Mesh** | Kademlia DHT discovery via `libp2p` with TCP + WebSocket + Yamux multiplexing and Noise encryption. |
|
|
@@ -263,13 +264,21 @@ await bridge.connect();
|
|
|
263
264
|
Built-in patterns with multi-layer verification:
|
|
264
265
|
|
|
265
266
|
```typescript
|
|
266
|
-
import { PII_PATTERNS } from "@nekzus/liop/server";
|
|
267
|
+
import { PII_PATTERNS, PII_PRESETS } from "@nekzus/liop/server";
|
|
267
268
|
|
|
268
269
|
// Available patterns:
|
|
269
270
|
PII_PATTERNS.EMAIL // RFC 5322 compliant, excludes @example.com/@test.com
|
|
270
271
|
PII_PATTERNS.CREDIT_CARD // Visa/MC/Amex + Luhn algorithm validation
|
|
271
272
|
PII_PATTERNS.IP_ADDRESS // IPv4 with octet range validation (excludes localhost)
|
|
272
273
|
PII_PATTERNS.PHONE // International phone formats with digit-length validation
|
|
274
|
+
PII_PATTERNS.SSN // USA Social Security Number rules (blocks 000 segments)
|
|
275
|
+
PII_PATTERNS.IBAN // ISO 7064 Modulo 97-10 algorithm precision via BigInt
|
|
276
|
+
PII_PATTERNS.PASSPORT_MRZ // Strict 44-character TD3 international passport MRZ string
|
|
277
|
+
|
|
278
|
+
// Pre-built Regional Presets ready to drop-in via LiopServer(options):
|
|
279
|
+
PII_PRESETS.GLOBAL_STRICT
|
|
280
|
+
PII_PRESETS.US_COMPLIANT
|
|
281
|
+
PII_PRESETS.EU_GDPR
|
|
273
282
|
```
|
|
274
283
|
|
|
275
284
|
### Forbidden Keys
|
|
@@ -292,7 +301,7 @@ const server = new LiopServer(info, {
|
|
|
292
301
|
The following shows a complete Logic-Injection-on-Origin execution cycle (handled internally by the SDK):
|
|
293
302
|
|
|
294
303
|
```
|
|
295
|
-
1. LLM generates JavaScript analysis code wrapped in
|
|
304
|
+
1. LLM generates JavaScript analysis code wrapped in @LIOP / @END boundaries
|
|
296
305
|
2. LiopServer receives the payload via tools/call (JSON-RPC or direct)
|
|
297
306
|
3. Guardian AST inspects for sandbox escapes (zero-time heuristic analysis)
|
|
298
307
|
4. Code executes inside a V8 isolate with CPU fuel limits (no Node.js globals)
|
package/dist/bin/agent.js
CHANGED
|
@@ -2,9 +2,105 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { multiaddr } from "@multiformats/multiaddr";
|
|
5
6
|
import { LiopMcpRouter } from "../gateway/router.js";
|
|
6
7
|
import { MeshNode } from "../mesh/index.js";
|
|
7
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
|
+
}
|
|
8
104
|
/**
|
|
9
105
|
* LIOP Agent (Zero-Config CLI)
|
|
10
106
|
*
|
|
@@ -15,6 +111,8 @@ import { LiopServer } from "../server/index.js";
|
|
|
15
111
|
* No hardcoded tools, PeerIDs, or port mappings.
|
|
16
112
|
*/
|
|
17
113
|
async function main() {
|
|
114
|
+
const buildTime = new Date().toISOString();
|
|
115
|
+
log.info(`[LIOP-Agent] 🚀 Version 1.2.0-alpha.9 | Build: ${buildTime}`);
|
|
18
116
|
const liopDir = path.join(os.homedir(), ".liop");
|
|
19
117
|
const identityPath = path.join(liopDir, "identity.json");
|
|
20
118
|
if (!fs.existsSync(liopDir)) {
|
|
@@ -27,31 +125,41 @@ async function main() {
|
|
|
27
125
|
if (args.length > 0) {
|
|
28
126
|
bootstrapNodes = args.filter((a) => a.startsWith("/"));
|
|
29
127
|
}
|
|
30
|
-
//
|
|
31
|
-
if (bootstrapNodes.length === 0 && process.env.LIOP_BOOTSTRAP) {
|
|
32
|
-
bootstrapNodes.push(process.env.LIOP_BOOTSTRAP.trim());
|
|
33
|
-
}
|
|
34
|
-
// Convenience: Try to read nexus.multiaddr from multiple locations
|
|
128
|
+
// Priority 1: Physical Beacons (Industrial Pattern) - DETERMINISTIC & INSTANT
|
|
35
129
|
if (bootstrapNodes.length === 0) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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) {
|
|
47
145
|
try {
|
|
48
|
-
if (fs.existsSync(
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
159
|
}
|
|
160
|
+
// If we found any beacons in this directory, we consider discovery successful for this layer
|
|
161
|
+
if (bootstrapNodes.length > 0)
|
|
162
|
+
break;
|
|
55
163
|
}
|
|
56
164
|
}
|
|
57
165
|
catch (_e) {
|
|
@@ -59,78 +167,141 @@ async function main() {
|
|
|
59
167
|
}
|
|
60
168
|
}
|
|
61
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
|
+
});
|
|
62
203
|
// If no bootstrap nodes found, the agent operates in standalone mode.
|
|
63
204
|
// It will only serve local tools until peers are discovered.
|
|
64
205
|
if (bootstrapNodes.length === 0) {
|
|
65
|
-
|
|
66
|
-
|
|
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.");
|
|
67
208
|
}
|
|
68
209
|
// Initialize local server node (lightweight, no tools registered locally)
|
|
69
210
|
const liopServer = new LiopServer({
|
|
70
211
|
name: "@nekzus/liop-agent",
|
|
71
212
|
version: "1.0.0",
|
|
72
213
|
});
|
|
214
|
+
// Enable Zero-Shot Autonomy (Industrial Prompt Injection)
|
|
215
|
+
liopServer.enableZeroShotAutonomy();
|
|
73
216
|
// 2. Mesh Node Configuration
|
|
74
217
|
const meshNode = new MeshNode({
|
|
75
218
|
identityPath: identityPath,
|
|
76
219
|
bootstrapNodes: bootstrapNodes,
|
|
220
|
+
addressMapper: industrialAddressMapper,
|
|
77
221
|
});
|
|
78
222
|
// Start P2P Mesh
|
|
79
223
|
await meshNode.start();
|
|
80
224
|
// 3. Initialize the Dynamic Router
|
|
81
225
|
// No hardcoded tools — all discovery happens via liop:manifest protocol
|
|
82
226
|
const router = new LiopMcpRouter(liopServer, meshNode);
|
|
83
|
-
// Proactive Notification to Claude Desktop when tools are discovered dynamically
|
|
227
|
+
// Proactive Notification to Claude Desktop when tools/resources are discovered dynamically
|
|
84
228
|
router.onToolsChanged = () => {
|
|
85
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`);
|
|
86
231
|
};
|
|
87
|
-
// Initial warming period (2s) then
|
|
88
|
-
//
|
|
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.
|
|
89
235
|
setTimeout(() => {
|
|
90
236
|
// biome-ignore lint/suspicious/noExplicitAny: access internal for telemetry
|
|
91
237
|
const rtSize = meshNode.getRoutingTableSize?.() || 0;
|
|
92
|
-
|
|
238
|
+
log.info(`[LIOP-Agent] Warm-up complete. Routing Table size: ${rtSize}`);
|
|
93
239
|
router.refreshManifestCache(true).catch(() => { });
|
|
94
240
|
}, 2000);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
});
|
|
99
270
|
process.stdout.on("error", (err) => {
|
|
100
271
|
if (err.code === "EPIPE") {
|
|
101
272
|
process.exit(0); // Graceful exit when Claude Desktop disconnects
|
|
102
273
|
}
|
|
103
274
|
});
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
275
|
+
rl.on("line", async (line) => {
|
|
276
|
+
const trimmed = line.trim();
|
|
277
|
+
if (!trimmed)
|
|
107
278
|
return;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
if (response) {
|
|
115
|
-
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
116
|
-
}
|
|
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`);
|
|
117
285
|
}
|
|
118
286
|
}
|
|
119
|
-
catch (_err) {
|
|
120
|
-
// Silent catch for binary noise
|
|
121
|
-
}
|
|
122
287
|
}
|
|
288
|
+
catch (_err) {
|
|
289
|
+
// Silent catch for binary noise or malformed lines
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
rl.on("close", () => {
|
|
293
|
+
process.exit(0);
|
|
123
294
|
});
|
|
124
295
|
// Status directed only to stderr
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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");
|
|
128
299
|
process.on("SIGINT", async () => {
|
|
129
300
|
await meshNode.stop();
|
|
130
301
|
process.exit(0);
|
|
131
302
|
});
|
|
132
303
|
}
|
|
133
304
|
main().catch((err) => {
|
|
134
|
-
|
|
305
|
+
log.error(`[LIOP-Agent] Fatal Error: ${err.message}`);
|
|
135
306
|
process.exit(1);
|
|
136
307
|
});
|
package/dist/bridge/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { LiopServer } from "../server/index.js";
|
|
3
|
+
import { log } from "../utils/logger.js";
|
|
3
4
|
/**
|
|
4
5
|
* LIOP MCP Bridge
|
|
5
6
|
* A bi-directional bridge that allows legacy MCP servers to join the LIOP mesh,
|
|
@@ -14,11 +15,11 @@ export class LiopMcpBridge {
|
|
|
14
15
|
// Determine mode: Exposing LIOP to MCP (Claude) or Wrapping MCP to LIOP (Mesh)
|
|
15
16
|
if (source instanceof LiopServer) {
|
|
16
17
|
this.liopServer = source;
|
|
17
|
-
|
|
18
|
+
log.info("[LIOP-Bridge] Mode: EXPOSE (LIOP -> MCP Stdio)");
|
|
18
19
|
}
|
|
19
20
|
else if (source instanceof McpServer) {
|
|
20
21
|
this.legacyMcpServer = source;
|
|
21
|
-
|
|
22
|
+
log.info("[LIOP-Bridge] Mode: WRAP (Legacy MCP -> LIOP Mesh)");
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
@@ -47,7 +48,7 @@ export class LiopMcpBridge {
|
|
|
47
48
|
return null;
|
|
48
49
|
if (method === "initialize") {
|
|
49
50
|
return this.successResponse(id, {
|
|
50
|
-
protocolVersion: "2025-
|
|
51
|
+
protocolVersion: "2025-11-25",
|
|
51
52
|
capabilities: {
|
|
52
53
|
prompts: {},
|
|
53
54
|
resources: {},
|
|
@@ -167,7 +168,7 @@ export class LiopMcpBridge {
|
|
|
167
168
|
return true;
|
|
168
169
|
}
|
|
169
170
|
catch (e) {
|
|
170
|
-
|
|
171
|
+
log.info("[LIOP-Bridge] ZK-Verifier Failure:", e);
|
|
171
172
|
return false;
|
|
172
173
|
}
|
|
173
174
|
}
|
|
@@ -221,7 +222,7 @@ export class LiopMcpBridge {
|
|
|
221
222
|
terminal: false,
|
|
222
223
|
});
|
|
223
224
|
const shutdown = async () => {
|
|
224
|
-
|
|
225
|
+
log.info("[LIOP-Bridge] Disconnecting session...");
|
|
225
226
|
if (this.liopServer)
|
|
226
227
|
await this.liopServer.close();
|
|
227
228
|
process.exit(0);
|
|
@@ -240,7 +241,7 @@ export class LiopMcpBridge {
|
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
catch (e) {
|
|
243
|
-
|
|
244
|
+
log.error(`[LIOP-Bridge] Error: ${e.message}`);
|
|
244
245
|
}
|
|
245
246
|
});
|
|
246
247
|
}
|
package/dist/bridge/stream.js
CHANGED
|
@@ -3,6 +3,7 @@ import { serve } from "@hono/node-server";
|
|
|
3
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
4
|
import { Hono } from "hono";
|
|
5
5
|
import { cors } from "hono/cors";
|
|
6
|
+
import { log } from "../utils/logger.js";
|
|
6
7
|
import { LiopMcpBridge } from "./index.js";
|
|
7
8
|
const DEFAULT_MAX_SESSIONS_PER_IP = 10;
|
|
8
9
|
const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
@@ -53,7 +54,7 @@ export class LiopStreamBridge {
|
|
|
53
54
|
lastActivity: Date.now(),
|
|
54
55
|
clientIp,
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
+
log.info(`[LIOP-StreamBridge] Session opened: ${sessionId} (IP: ${clientIp})`);
|
|
57
58
|
},
|
|
58
59
|
});
|
|
59
60
|
// Wire the transport's incoming messages to the LiopMcpBridge JSON-RPC router
|
|
@@ -72,13 +73,13 @@ export class LiopStreamBridge {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
catch (err) {
|
|
75
|
-
|
|
76
|
+
log.info("[LIOP-StreamBridge] JSON-RPC error:", err.message);
|
|
76
77
|
}
|
|
77
78
|
};
|
|
78
79
|
transport.onclose = () => {
|
|
79
80
|
if (transport.sessionId) {
|
|
80
81
|
this.activeSessions.delete(transport.sessionId);
|
|
81
|
-
|
|
82
|
+
log.info(`[LIOP-StreamBridge] Session closed: ${transport.sessionId}`);
|
|
82
83
|
}
|
|
83
84
|
};
|
|
84
85
|
return transport;
|
|
@@ -109,7 +110,7 @@ export class LiopStreamBridge {
|
|
|
109
110
|
const now = Date.now();
|
|
110
111
|
for (const [sessionId, entry] of this.activeSessions) {
|
|
111
112
|
if (now - entry.lastActivity > this.sessionTimeoutMs) {
|
|
112
|
-
|
|
113
|
+
log.info(`[LIOP-StreamBridge] Evicting idle session: ${sessionId}`);
|
|
113
114
|
entry.transport.close().catch(() => {
|
|
114
115
|
/* Swallow close errors */
|
|
115
116
|
});
|
|
@@ -124,10 +125,9 @@ export class LiopStreamBridge {
|
|
|
124
125
|
const auth = c.req.header("Authorization");
|
|
125
126
|
const expectedToken = process.env.ZERO_TRUST_TOKEN;
|
|
126
127
|
if (expectedToken) {
|
|
127
|
-
if (!auth ||
|
|
128
|
-
!auth.startsWith("Bearer ") ||
|
|
128
|
+
if (!auth?.startsWith("Bearer ") ||
|
|
129
129
|
auth.split(" ")[1] !== expectedToken) {
|
|
130
|
-
|
|
130
|
+
log.info("[LIOP-StreamBridge] ALERT: Access denied - Invalid Zero-Trust token.");
|
|
131
131
|
return c.json({ error: "Unauthorized: LIOP Zero-Trust Policy Enforced" }, 401);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -149,7 +149,7 @@ export class LiopStreamBridge {
|
|
|
149
149
|
// Explicitly clean up the session from the Map.
|
|
150
150
|
if (c.req.method === "DELETE") {
|
|
151
151
|
this.activeSessions.delete(sessionId);
|
|
152
|
-
|
|
152
|
+
log.info(`[LIOP-StreamBridge] Session closed (DELETE): ${sessionId}`);
|
|
153
153
|
}
|
|
154
154
|
return response;
|
|
155
155
|
}
|
|
@@ -158,7 +158,7 @@ export class LiopStreamBridge {
|
|
|
158
158
|
const clientIp = this.getClientIp(c);
|
|
159
159
|
const currentSessions = this.countSessionsByIp(clientIp);
|
|
160
160
|
if (currentSessions >= this.maxSessionsPerIp) {
|
|
161
|
-
|
|
161
|
+
log.info(`[LIOP-StreamBridge] Rate limit hit for IP: ${clientIp} (${currentSessions} sessions)`);
|
|
162
162
|
return c.json({ error: "Too Many Sessions: Rate limit exceeded" }, 429);
|
|
163
163
|
}
|
|
164
164
|
const transport = this.createSessionTransport(clientIp);
|
|
@@ -177,7 +177,7 @@ export class LiopStreamBridge {
|
|
|
177
177
|
fetch: this.app.fetch,
|
|
178
178
|
port: listenPort,
|
|
179
179
|
}, (info) => {
|
|
180
|
-
|
|
180
|
+
log.info(`[LIOP-StreamBridge] Streamable HTTP Gateway on http://localhost:${info.port}/mcp`);
|
|
181
181
|
resolve();
|
|
182
182
|
});
|
|
183
183
|
});
|
|
@@ -196,7 +196,7 @@ export class LiopStreamBridge {
|
|
|
196
196
|
}
|
|
197
197
|
if (this.httpServer) {
|
|
198
198
|
this.httpServer.close();
|
|
199
|
-
|
|
199
|
+
log.info("[LIOP-StreamBridge] HTTP ports released.");
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
}
|