@nekzus/liop 2.0.0-alpha.1 → 2.0.0-alpha.2
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/bin/agent.d.ts +0 -1
- package/dist/bin/agent.js +5 -306
- package/dist/bin/agent.js.map +1 -0
- package/dist/{bridge/stream.d.ts → bridge.d.ts} +44 -3
- package/dist/bridge.js +2 -0
- package/dist/bridge.js.map +1 -0
- package/dist/chunk-4ABAFG44.js +33 -0
- package/dist/chunk-4ABAFG44.js.map +1 -0
- package/dist/chunk-ANFXJGMP.js +2 -0
- package/dist/chunk-ANFXJGMP.js.map +1 -0
- package/dist/chunk-DBXGYHKY.js +2 -0
- package/dist/chunk-DBXGYHKY.js.map +1 -0
- package/dist/chunk-HM77MWB6.js +2 -0
- package/dist/chunk-HM77MWB6.js.map +1 -0
- package/dist/chunk-HNDVAKEK.js +24 -0
- package/dist/chunk-HNDVAKEK.js.map +1 -0
- package/dist/chunk-HQZHZM6U.js +2 -0
- package/dist/chunk-HQZHZM6U.js.map +1 -0
- package/dist/chunk-P52IE4L6.js +2 -0
- package/dist/chunk-P52IE4L6.js.map +1 -0
- package/dist/chunk-PIBCW4BD.js +13 -0
- package/dist/chunk-PIBCW4BD.js.map +1 -0
- package/dist/chunk-PPCOS2NU.js +2 -0
- package/dist/chunk-PPCOS2NU.js.map +1 -0
- package/dist/chunk-RWRRBYG4.js +2 -0
- package/dist/chunk-RWRRBYG4.js.map +1 -0
- package/dist/chunk-S6RJHZV2.js +2 -0
- package/dist/chunk-S6RJHZV2.js.map +1 -0
- package/dist/chunk-UVTEJYHN.js +2 -0
- package/dist/chunk-UVTEJYHN.js.map +1 -0
- package/dist/chunk-X6FJATUE.js +29 -0
- package/dist/chunk-X6FJATUE.js.map +1 -0
- package/dist/chunk-XLVRRGOX.js +3 -0
- package/dist/chunk-XLVRRGOX.js.map +1 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +2 -0
- package/dist/client.js.map +1 -0
- package/dist/{gateway/router.d.ts → gateway.d.ts} +30 -5
- package/dist/gateway.js +2 -0
- package/dist/gateway.js.map +1 -0
- package/dist/{client/index.d.ts → index-CyxNLlz7.d.ts} +24 -5
- package/dist/index.d.ts +313 -12
- package/dist/index.js +31 -12
- package/dist/index.js.map +1 -0
- package/dist/kyber-2WDOTUQX.js +2 -0
- package/dist/kyber-2WDOTUQX.js.map +1 -0
- package/dist/{mesh/node.d.ts → mesh.d.ts} +5 -3
- package/dist/mesh.js +2 -0
- package/dist/mesh.js.map +1 -0
- package/dist/{server/index.d.ts → server.d.ts} +125 -12
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/types.d.ts +17 -14
- package/dist/types.js +2 -26
- package/dist/types.js.map +1 -0
- package/dist/{crypto/verifier.d.ts → verifier-DTCD9imJ.d.ts} +3 -1
- package/dist/verifier-RQRYXA4C.js +2 -0
- package/dist/verifier-RQRYXA4C.js.map +1 -0
- package/dist/workers/logic-execution.d.ts +4 -2
- package/dist/workers/logic-execution.js +2 -123
- package/dist/workers/logic-execution.js.map +1 -0
- package/dist/workers/zk-verifier.d.ts +4 -2
- package/dist/workers/zk-verifier.js +2 -98
- package/dist/workers/zk-verifier.js.map +1 -0
- package/package.json +31 -18
- package/dist/bridge/index.d.ts +0 -37
- package/dist/bridge/index.js +0 -249
- package/dist/bridge/stream.js +0 -210
- package/dist/client/index.js +0 -275
- package/dist/crypto/logic-image-id.d.ts +0 -3
- package/dist/crypto/logic-image-id.js +0 -27
- package/dist/crypto/verifier.js +0 -97
- package/dist/economy/estimator.d.ts +0 -53
- package/dist/economy/estimator.js +0 -69
- package/dist/economy/index.d.ts +0 -5
- package/dist/economy/index.js +0 -3
- package/dist/economy/otel.d.ts +0 -38
- package/dist/economy/otel.js +0 -100
- package/dist/economy/telemetry.d.ts +0 -77
- package/dist/economy/telemetry.js +0 -224
- package/dist/errors.d.ts +0 -14
- package/dist/errors.js +0 -19
- package/dist/gateway/hybrid.d.ts +0 -23
- package/dist/gateway/hybrid.js +0 -199
- package/dist/gateway/router.js +0 -1054
- package/dist/mesh/index.d.ts +0 -1
- package/dist/mesh/index.js +0 -1
- package/dist/mesh/node.js +0 -853
- package/dist/prompts/adapters.d.ts +0 -16
- package/dist/prompts/adapters.js +0 -55
- package/dist/rpc/client.d.ts +0 -22
- package/dist/rpc/client.js +0 -40
- package/dist/rpc/codec/lpm.d.ts +0 -20
- package/dist/rpc/codec/lpm.js +0 -36
- package/dist/rpc/crypto/aes.d.ts +0 -22
- package/dist/rpc/crypto/aes.js +0 -47
- package/dist/rpc/crypto/kyber.d.ts +0 -27
- package/dist/rpc/crypto/kyber.js +0 -70
- package/dist/rpc/proto.d.ts +0 -2
- package/dist/rpc/proto.js +0 -33
- package/dist/rpc/server.d.ts +0 -13
- package/dist/rpc/server.js +0 -50
- package/dist/rpc/tls.d.ts +0 -26
- package/dist/rpc/tls.js +0 -54
- package/dist/rpc/types.d.ts +0 -28
- package/dist/rpc/types.js +0 -5
- package/dist/sandbox/guardian.d.ts +0 -18
- package/dist/sandbox/guardian.js +0 -58
- package/dist/sandbox/wasi.d.ts +0 -36
- package/dist/sandbox/wasi.js +0 -233
- package/dist/security/guardian.d.ts +0 -22
- package/dist/security/guardian.js +0 -52
- package/dist/security/zk.d.ts +0 -37
- package/dist/security/zk.js +0 -76
- package/dist/server/index.js +0 -1047
- package/dist/server/ner-scanner.d.ts +0 -29
- package/dist/server/ner-scanner.js +0 -141
- package/dist/server/pii.d.ts +0 -66
- package/dist/server/pii.js +0 -428
- package/dist/utils/logger.d.ts +0 -21
- package/dist/utils/logger.js +0 -70
- package/dist/utils/mcpCompact.d.ts +0 -11
- package/dist/utils/mcpCompact.js +0 -29
package/dist/server/index.js
DELETED
|
@@ -1,1047 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import * as grpc from "@grpc/grpc-js";
|
|
7
|
-
import { FixedQueue, Piscina } from "piscina";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
10
|
-
import { MeshNode } from "../mesh/node.js";
|
|
11
|
-
import { LiopRpcServer } from "../rpc/server.js";
|
|
12
|
-
import { log } from "../utils/logger.js";
|
|
13
|
-
import { NerScanner } from "./ner-scanner.js";
|
|
14
|
-
import { PII_PATTERNS, PII_PRESETS, PiiScanner } from "./pii.js";
|
|
15
|
-
export { NerScanner, PII_PATTERNS, PII_PRESETS, PiiScanner };
|
|
16
|
-
/**
|
|
17
|
-
* When enabled, `payload` tools that are not LIOP v1 envelopes are passed through to the
|
|
18
|
-
* registered handler unchanged (no worker extraction). Default off for strict protocol tests.
|
|
19
|
-
*/
|
|
20
|
-
function respectPlainToolPayload() {
|
|
21
|
-
const v = process.env.LIOP_RESPECT_PLAIN_TOOL_PAYLOAD?.toLowerCase().trim();
|
|
22
|
-
return v === "1" || v === "true" || v === "yes";
|
|
23
|
-
}
|
|
24
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
export class LiopServer {
|
|
26
|
-
serverInfo;
|
|
27
|
-
config;
|
|
28
|
-
logicCache = new Map();
|
|
29
|
-
connectionStats = new Map();
|
|
30
|
-
CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
31
|
-
THROTTLE_THRESHOLD = 5;
|
|
32
|
-
THROTTLE_COOLDOWN_MS = 60 * 1000; // 60 seconds
|
|
33
|
-
// [OWASP-A01] Sliding window rate limiter — prevents micro-query exfiltration
|
|
34
|
-
toolCallWindows = new Map();
|
|
35
|
-
toolCallMaxPerWindow;
|
|
36
|
-
toolCallWindowMs;
|
|
37
|
-
tools = new Map();
|
|
38
|
-
resources = new Map();
|
|
39
|
-
prompts = new Map();
|
|
40
|
-
activeSchema = null;
|
|
41
|
-
sandboxRecords = [];
|
|
42
|
-
piiScanner;
|
|
43
|
-
workerPool;
|
|
44
|
-
meshNode = null;
|
|
45
|
-
rpcServer = null;
|
|
46
|
-
boundPort = null;
|
|
47
|
-
sessions = new Map();
|
|
48
|
-
// Compact envelope: @LIOP{target,name}\n<code>\n@END
|
|
49
|
-
static LIOP_COMPACT_REGEX = /@LIOP\{(?<target>[^,}]+)(?:,(?<name>[^}]*))?\}\n(?<logic>[\s\S]*?)\n@END/m;
|
|
50
|
-
extractLogic(payload) {
|
|
51
|
-
const compact = payload.match(LiopServer.LIOP_COMPACT_REGEX);
|
|
52
|
-
return compact?.groups?.logic ? compact.groups.logic.trim() : null;
|
|
53
|
-
}
|
|
54
|
-
parseUnknownJson(input) {
|
|
55
|
-
if (typeof input !== "string")
|
|
56
|
-
return input;
|
|
57
|
-
const trimmed = input.trim();
|
|
58
|
-
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
59
|
-
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
60
|
-
try {
|
|
61
|
-
return JSON.parse(trimmed);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return input;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return input;
|
|
68
|
-
}
|
|
69
|
-
runPreflightPolicy(_toolName, logic, policy) {
|
|
70
|
-
if (!policy)
|
|
71
|
-
return null;
|
|
72
|
-
const compact = logic.replace(/\s+/g, " ");
|
|
73
|
-
if (policy.enforceAggregationFirst) {
|
|
74
|
-
const rowExtractionPatterns = [
|
|
75
|
-
// Block raw record dumps but allow safe aggregation chains
|
|
76
|
-
// (.reduce, .length, .filter().length, .every, .some)
|
|
77
|
-
/return\s+env\.records(?!\s*\.\s*(?:reduce|length|filter|every|some|find)\b)/i,
|
|
78
|
-
/return\s*\{[\s\S]*\b(accounts|patients|rows|records)\s*:\s*env\.records(?!\s*\.\s*(?:reduce|length|filter)\b)/i,
|
|
79
|
-
];
|
|
80
|
-
if (rowExtractionPatterns.some((p) => p.test(compact))) {
|
|
81
|
-
return "Preflight policy rejected: potential row-level export pattern detected.";
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (policy.preflightDenyPatterns?.some((p) => p.test(compact))) {
|
|
85
|
-
return "Preflight policy rejected: custom deny pattern matched.";
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
validateOutputPolicy(toolName, output, policy) {
|
|
90
|
-
if (!policy)
|
|
91
|
-
return null;
|
|
92
|
-
const parsed = this.parseUnknownJson(output);
|
|
93
|
-
if (policy.outputSchema) {
|
|
94
|
-
// SEC-HARDENING: Force strict mode on ZodObject schemas to prevent
|
|
95
|
-
// key aliasing bypasses via .passthrough(). However, respect schemas
|
|
96
|
-
// that explicitly use .catchall() — calling .strict() would override
|
|
97
|
-
// the catchall with ZodNever, destroying the developer's intent.
|
|
98
|
-
const effectiveSchema = (() => {
|
|
99
|
-
if (!(policy.outputSchema instanceof z.ZodObject)) {
|
|
100
|
-
return policy.outputSchema;
|
|
101
|
-
}
|
|
102
|
-
const obj = policy.outputSchema;
|
|
103
|
-
// If schema has an explicit catchall (not ZodNever), respect it
|
|
104
|
-
if (!(obj._def.catchall instanceof z.ZodNever)) {
|
|
105
|
-
return obj;
|
|
106
|
-
}
|
|
107
|
-
// Otherwise force strict to block unrecognized keys by default
|
|
108
|
-
return obj.strict();
|
|
109
|
-
})();
|
|
110
|
-
const schemaResult = effectiveSchema.safeParse(parsed);
|
|
111
|
-
if (!schemaResult.success) {
|
|
112
|
-
// SEC-CRITICAL: Never expose rejected data in error messages.
|
|
113
|
-
// Only report the structural violation (unrecognized keys, type mismatches).
|
|
114
|
-
return `[LIOP] Output schema violation for ${toolName}: ${schemaResult.error.issues
|
|
115
|
-
.map((i) => `${i.path.join(".") || "<root>"} ${i.message}`)
|
|
116
|
-
.join("; ")}. HINT: Your output must conform to the declared schema. Use 'env.records' to access the dataset and return only allowed fields.`;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (policy.enforceAggregationFirst &&
|
|
120
|
-
this.violatesAggregationFirstPolicy(this.unwrapForAggregationPolicyScan(parsed), policy.enforceAggregationFirst)) {
|
|
121
|
-
return "Aggregation-First Policy Violation: row-level export blocked. HINT: Use .reduce() to produce a flat {key:value} object. Do NOT use .map() to create arrays of objects.";
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Proxied tools stringify a full MCP CallToolResult (`{ content: [...] }`).
|
|
127
|
-
* Aggregation-first heuristics must scan the inner business JSON, not the MCP envelope
|
|
128
|
-
* (otherwise `content` looks like a tabular array of objects and everything blocks).
|
|
129
|
-
*/
|
|
130
|
-
unwrapForAggregationPolicyScan(input) {
|
|
131
|
-
if (typeof input === "string") {
|
|
132
|
-
const trimmed = input.trim();
|
|
133
|
-
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
134
|
-
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
135
|
-
try {
|
|
136
|
-
return this.unwrapForAggregationPolicyScan(JSON.parse(trimmed));
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return input;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return input;
|
|
143
|
-
}
|
|
144
|
-
if (!input || typeof input !== "object") {
|
|
145
|
-
return input;
|
|
146
|
-
}
|
|
147
|
-
const rec = input;
|
|
148
|
-
if (!Array.isArray(rec.content) || rec.content.length === 0) {
|
|
149
|
-
return input;
|
|
150
|
-
}
|
|
151
|
-
const texts = [];
|
|
152
|
-
for (const part of rec.content) {
|
|
153
|
-
if (part && typeof part === "object" && "text" in part) {
|
|
154
|
-
const t = part.text;
|
|
155
|
-
if (typeof t === "string") {
|
|
156
|
-
texts.push(t);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (texts.length === 0) {
|
|
161
|
-
return input;
|
|
162
|
-
}
|
|
163
|
-
const joined = texts.length === 1 ? texts[0] : texts.join("\n");
|
|
164
|
-
return this.unwrapForAggregationPolicyScan(joined);
|
|
165
|
-
}
|
|
166
|
-
violatesAggregationFirstPolicy(input, policyObj) {
|
|
167
|
-
const maxRows = typeof policyObj === "object" &&
|
|
168
|
-
typeof policyObj.maxOutputRows === "number"
|
|
169
|
-
? policyObj.maxOutputRows
|
|
170
|
-
: 10;
|
|
171
|
-
const allowPrimitives = typeof policyObj === "object" &&
|
|
172
|
-
typeof policyObj.allowPrimitiveArrays === "boolean"
|
|
173
|
-
? policyObj.allowPrimitiveArrays
|
|
174
|
-
: true;
|
|
175
|
-
if (typeof input === "string") {
|
|
176
|
-
const trimmed = input.trim();
|
|
177
|
-
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
|
|
178
|
-
(trimmed.startsWith("[") && trimmed.endsWith("]"))) {
|
|
179
|
-
try {
|
|
180
|
-
return this.violatesAggregationFirstPolicy(JSON.parse(trimmed), policyObj);
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return false;
|
|
187
|
-
}
|
|
188
|
-
if (Array.isArray(input)) {
|
|
189
|
-
if (input.length > 0 &&
|
|
190
|
-
input.every((item) => typeof item === "object" && item !== null)) {
|
|
191
|
-
// Treat tabular row export as non-aggregated leakage risk if above threshold.
|
|
192
|
-
if (input.length > maxRows) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
return input.some((item) => this.violatesAggregationFirstPolicy(item, policyObj));
|
|
196
|
-
}
|
|
197
|
-
if (input.length > 0 &&
|
|
198
|
-
input.every((item) => typeof item !== "object" || item === null)) {
|
|
199
|
-
if (!allowPrimitives)
|
|
200
|
-
return true;
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
return input.some((item) => this.violatesAggregationFirstPolicy(item, policyObj));
|
|
204
|
-
}
|
|
205
|
-
if (input && typeof input === "object") {
|
|
206
|
-
const keys = Object.keys(input);
|
|
207
|
-
// Treat flat dictionary with too many keys as non-aggregated leakage risk (Dynamic Key Bypass).
|
|
208
|
-
if (keys.length > maxRows) {
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
return Object.values(input).some((value) => this.violatesAggregationFirstPolicy(value, policyObj));
|
|
212
|
-
}
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
constructor(serverInfo, config) {
|
|
216
|
-
this.serverInfo = serverInfo;
|
|
217
|
-
this.config = config;
|
|
218
|
-
const nerScanner = this.config?.security?.enableNerScanning
|
|
219
|
-
? new NerScanner()
|
|
220
|
-
: null;
|
|
221
|
-
this.piiScanner = new PiiScanner(this.config?.security?.piiPatterns ?? PII_PRESETS.GLOBAL_STRICT, this.config?.security?.forbiddenKeys ?? [
|
|
222
|
-
"id",
|
|
223
|
-
"name",
|
|
224
|
-
"fullName",
|
|
225
|
-
"firstName",
|
|
226
|
-
"lastName",
|
|
227
|
-
"address",
|
|
228
|
-
"street",
|
|
229
|
-
"city",
|
|
230
|
-
"postalCode",
|
|
231
|
-
"zipCode",
|
|
232
|
-
"phone",
|
|
233
|
-
"email",
|
|
234
|
-
"ssn",
|
|
235
|
-
"accountHolder",
|
|
236
|
-
"accountNumber",
|
|
237
|
-
"account_number",
|
|
238
|
-
"password",
|
|
239
|
-
"token",
|
|
240
|
-
"secret",
|
|
241
|
-
"privateKey",
|
|
242
|
-
], nerScanner);
|
|
243
|
-
// [OWASP-A01] Rate limit: config > env > default (30 calls/min)
|
|
244
|
-
const rlConfig = this.config?.security?.rateLimit;
|
|
245
|
-
this.toolCallWindowMs =
|
|
246
|
-
rlConfig?.windowMs ??
|
|
247
|
-
Number.parseInt(process.env.LIOP_RATE_LIMIT_WINDOW_MS ?? "60000", 10);
|
|
248
|
-
this.toolCallMaxPerWindow =
|
|
249
|
-
rlConfig?.maxPerWindow ??
|
|
250
|
-
Number.parseInt(process.env.LIOP_RATE_LIMIT_MAX ?? "30", 10);
|
|
251
|
-
// Initialize Zero-Blocking Worker Pool for Heavy Cryptography & Sandboxing
|
|
252
|
-
const isTS = import.meta.url.endsWith(".ts");
|
|
253
|
-
const workerExt = isTS ? ".ts" : ".js";
|
|
254
|
-
let execArgv = [];
|
|
255
|
-
if (isTS) {
|
|
256
|
-
try {
|
|
257
|
-
const req = createRequire(import.meta.url);
|
|
258
|
-
const tsxPkg = req.resolve("tsx/package.json");
|
|
259
|
-
const absoluteTsx = pathToFileURL(path.join(path.dirname(tsxPkg), "dist", "loader.mjs")).href;
|
|
260
|
-
execArgv = ["--import", absoluteTsx];
|
|
261
|
-
}
|
|
262
|
-
catch (_e) {
|
|
263
|
-
execArgv = ["--import", "tsx"];
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const isTest = process.env.NODE_ENV === "test" || process.env.VITEST;
|
|
267
|
-
// Sync capabilities to serverInfo for MCP Handshakes
|
|
268
|
-
if (this.config?.capabilities && !this.serverInfo.capabilities) {
|
|
269
|
-
this.serverInfo.capabilities = this.config.capabilities;
|
|
270
|
-
}
|
|
271
|
-
this.workerPool = new Piscina({
|
|
272
|
-
filename: path.resolve(__dirname, `../workers/logic-execution${workerExt}`),
|
|
273
|
-
minThreads: this.config?.workerPool?.minThreads ?? (isTest ? 0 : 2),
|
|
274
|
-
maxThreads: this.config?.workerPool?.maxThreads ?? (isTest ? 1 : 8),
|
|
275
|
-
idleTimeout: this.config?.workerPool?.idleTimeout ?? (isTest ? 500 : 5000),
|
|
276
|
-
maxQueue: "auto",
|
|
277
|
-
taskQueue: new FixedQueue(),
|
|
278
|
-
execArgv,
|
|
279
|
-
// [DoS Defense] Enforce hard memory ceiling per worker thread.
|
|
280
|
-
// Workers exceeding this limit are terminated by Node.js runtime.
|
|
281
|
-
resourceLimits: {
|
|
282
|
-
maxOldGenerationSizeMb: this.config?.workerPool?.maxHeapMb ??
|
|
283
|
-
Number.parseInt(process.env.LIOP_WORKER_MAX_HEAP_MB ?? "64", 10),
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
// [Token Economy] Auto-register LIOP protocol spec as a single Resource.
|
|
287
|
-
// This centralizes the envelope documentation that was previously
|
|
288
|
-
// duplicated in every tool description, reducing token overhead.
|
|
289
|
-
this.resource("LIOP Envelope Specification", "liop://protocol/envelope-spec", "Complete Logic-on-Origin envelope format, execution rules, and security constraints", "text/plain", () => Promise.resolve(this.buildEnvelopeSpec()));
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Builds the centralized LIOP envelope specification document.
|
|
293
|
-
* Served as a single Resource (liop://protocol/envelope-spec) instead
|
|
294
|
-
* of being duplicated across every tool description.
|
|
295
|
-
*/
|
|
296
|
-
buildEnvelopeSpec() {
|
|
297
|
-
const lines = [
|
|
298
|
-
"LIOP v1 Envelope Specification",
|
|
299
|
-
"================================",
|
|
300
|
-
"",
|
|
301
|
-
"FORMAT:",
|
|
302
|
-
"",
|
|
303
|
-
"Compact Envelope:",
|
|
304
|
-
" @LIOP{wasi_v1,TaskName}",
|
|
305
|
-
" <JavaScript code>",
|
|
306
|
-
" @END",
|
|
307
|
-
"",
|
|
308
|
-
"RUNTIME ENVIRONMENT:",
|
|
309
|
-
"- env.records: Array of data objects from the origin",
|
|
310
|
-
"- Must use 'return' to output results",
|
|
311
|
-
"- Zero-Trust WASI Sandbox (Node.js Worker Pool)",
|
|
312
|
-
"- Return aggregated objects, NOT raw row-level arrays",
|
|
313
|
-
"",
|
|
314
|
-
"SECURITY CONSTRAINTS:",
|
|
315
|
-
"- PII Egress Shield blocks raw identifiers in output",
|
|
316
|
-
"- Aggregation-First policy: prefer counts, averages, summaries",
|
|
317
|
-
"- AST Guardian: static analysis before execution",
|
|
318
|
-
];
|
|
319
|
-
if (this.config?.security?.forbiddenKeys?.length) {
|
|
320
|
-
lines.push(`- Restricted fields: ${this.config.security.forbiddenKeys.join(", ")}`);
|
|
321
|
-
}
|
|
322
|
-
lines.push("", "OPTIONAL PARAMETERS:", "- __liop_bypass_ast_cache: boolean (force AST re-evaluation)");
|
|
323
|
-
return lines.join("\n");
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Extracts a compact, human-readable field summary from a JSON Schema.
|
|
327
|
-
*
|
|
328
|
-
* Walks the schema structure to find actual data property names and types,
|
|
329
|
-
* rather than returning top-level schema metadata keys (type, items, etc.).
|
|
330
|
-
*
|
|
331
|
-
* Example output for a banking schema:
|
|
332
|
-
* "Array of {id(string), accountHolder(string), balance(number), transactions(array of {date(string), amount(number)})}"
|
|
333
|
-
*/
|
|
334
|
-
extractSchemaFieldSummary(schema, depth = 0) {
|
|
335
|
-
// Prevent excessive recursion in deeply nested schemas
|
|
336
|
-
if (depth > 3)
|
|
337
|
-
return "{...}";
|
|
338
|
-
const schemaType = schema.type;
|
|
339
|
-
const properties = schema.properties;
|
|
340
|
-
const items = schema.items;
|
|
341
|
-
// Object with properties → list field names with their types
|
|
342
|
-
if (properties) {
|
|
343
|
-
const fields = Object.entries(properties).map(([key, prop]) => {
|
|
344
|
-
const propType = prop.type;
|
|
345
|
-
if (propType === "array" && prop.items) {
|
|
346
|
-
const nested = this.extractSchemaFieldSummary(prop.items, depth + 1);
|
|
347
|
-
return `${key}(array of ${nested})`;
|
|
348
|
-
}
|
|
349
|
-
if (propType === "object" && prop.properties) {
|
|
350
|
-
const nested = this.extractSchemaFieldSummary(prop, depth + 1);
|
|
351
|
-
return `${key}(${nested})`;
|
|
352
|
-
}
|
|
353
|
-
return `${key}(${propType || "unknown"})`;
|
|
354
|
-
});
|
|
355
|
-
return `{${fields.join(", ")}}`;
|
|
356
|
-
}
|
|
357
|
-
// Array type → describe the items structure
|
|
358
|
-
if (schemaType === "array" && items) {
|
|
359
|
-
const itemsSummary = this.extractSchemaFieldSummary(items, depth + 1);
|
|
360
|
-
return `Array of ${itemsSummary}`;
|
|
361
|
-
}
|
|
362
|
-
// Simple type or unknown structure → fallback to key listing
|
|
363
|
-
if (schemaType)
|
|
364
|
-
return schemaType;
|
|
365
|
-
return Object.keys(schema).join(", ");
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Convenience alias for connectToMesh(), matching official documentation.
|
|
369
|
-
*/
|
|
370
|
-
async connect(options = {}) {
|
|
371
|
-
return this.connectToMesh(options);
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Register a new Tool
|
|
375
|
-
*/
|
|
376
|
-
tool(name, description, shape, handler, policy) {
|
|
377
|
-
if (this.tools.has(name)) {
|
|
378
|
-
throw new Error(`Tool already registered: ${name}`);
|
|
379
|
-
}
|
|
380
|
-
const schema = z.object(shape);
|
|
381
|
-
const generatedSchema = zodToJsonSchema(schema);
|
|
382
|
-
let finalDescription = description;
|
|
383
|
-
let finalHandler = handler;
|
|
384
|
-
// LIOP Zero-Shot Autonomy Middleware: Detect Logic-on-Origin tools
|
|
385
|
-
if (shape.payload && shape.payload instanceof z.ZodString) {
|
|
386
|
-
const blockedKeys = this.config?.security?.forbiddenKeys || [];
|
|
387
|
-
// [Token Economy] Centralized description: reference the protocol spec
|
|
388
|
-
// Resource instead of duplicating the full envelope format per tool.
|
|
389
|
-
// Same information, delivered once via liop://protocol/envelope-spec.
|
|
390
|
-
finalDescription +=
|
|
391
|
-
"\n\nPayload: LIOP v1 envelope (WASI sandbox)." +
|
|
392
|
-
" Format: @LIOP{wasi_v1,TaskName}\\n<JS code>\\n@END" +
|
|
393
|
-
" | Access data: env.records. Return aggregated object." +
|
|
394
|
-
" | Full spec: resource liop://protocol/envelope-spec";
|
|
395
|
-
if (blockedKeys.length > 0) {
|
|
396
|
-
finalDescription += `\nRestricted fields: ${blockedKeys.join(", ")}.`;
|
|
397
|
-
}
|
|
398
|
-
if (this.activeSchema) {
|
|
399
|
-
const schemaDigest = this.extractSchemaFieldSummary(this.activeSchema);
|
|
400
|
-
finalDescription += `\nData structure: ${schemaDigest}. Full schema: resource liop://schema/global`;
|
|
401
|
-
}
|
|
402
|
-
finalHandler = async (args, _extra) => {
|
|
403
|
-
const clientId = "global_connection"; // Simplify for now, treating the instance as one connection
|
|
404
|
-
const now = Date.now();
|
|
405
|
-
const stats = this.connectionStats.get(clientId) || {
|
|
406
|
-
failures: 0,
|
|
407
|
-
lastAttempt: 0,
|
|
408
|
-
};
|
|
409
|
-
if (stats.failures >= this.THROTTLE_THRESHOLD &&
|
|
410
|
-
now - stats.lastAttempt < this.THROTTLE_COOLDOWN_MS) {
|
|
411
|
-
return {
|
|
412
|
-
content: [
|
|
413
|
-
{
|
|
414
|
-
type: "text",
|
|
415
|
-
text: "LIOP_THROTTLED: Too many violations. Cooling down for 60 seconds.",
|
|
416
|
-
},
|
|
417
|
-
],
|
|
418
|
-
isError: true,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
const payloadValue = args
|
|
422
|
-
.payload;
|
|
423
|
-
const bypassCache = args.__liop_bypass_ast_cache === true;
|
|
424
|
-
const payloadHash = crypto
|
|
425
|
-
.createHash("sha256")
|
|
426
|
-
.update(payloadValue)
|
|
427
|
-
.digest("hex");
|
|
428
|
-
const logic = this.extractLogic(payloadValue);
|
|
429
|
-
const cached = this.logicCache.get(payloadHash);
|
|
430
|
-
if (!bypassCache &&
|
|
431
|
-
cached &&
|
|
432
|
-
now - cached.timestamp < this.CACHE_TTL_MS) {
|
|
433
|
-
// Hash verified. Skips boundaries check (already validated!). Extract logic directly.
|
|
434
|
-
if (logic) {
|
|
435
|
-
args.payload = logic;
|
|
436
|
-
// DELEGATE TO WORKER POOL: Parallel PQC & Sandboxing
|
|
437
|
-
const preflightReason = this.runPreflightPolicy(name, logic, policy);
|
|
438
|
-
if (preflightReason) {
|
|
439
|
-
return {
|
|
440
|
-
content: [{ type: "text", text: preflightReason }],
|
|
441
|
-
isError: true,
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
return await this.executeInWorkerPool(args, logic, name);
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
if (!logic) {
|
|
448
|
-
if (respectPlainToolPayload()) {
|
|
449
|
-
return await handler(args, _extra);
|
|
450
|
-
}
|
|
451
|
-
stats.failures++;
|
|
452
|
-
stats.lastAttempt = now;
|
|
453
|
-
this.connectionStats.set(clientId, stats);
|
|
454
|
-
return {
|
|
455
|
-
content: [
|
|
456
|
-
{
|
|
457
|
-
type: "text",
|
|
458
|
-
text: "Error: Malformed payload. Missing @LIOP boundary.\\nYou MUST wrap your logic exactly like this:\\n\\n@LIOP{wasi_v1,DynamicAudit}\\n// Your JS code here\\n@END",
|
|
459
|
-
},
|
|
460
|
-
],
|
|
461
|
-
isError: true,
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
// Logic check already performed above, extraction is guaranteed at this point.
|
|
466
|
-
// biome-ignore lint/style/noNonNullAssertion: safe extraction after check
|
|
467
|
-
const logic = this.extractLogic(args.payload);
|
|
468
|
-
// Extract pure logic and deliver it to the developer's function
|
|
469
|
-
args.payload = logic;
|
|
470
|
-
// DELEGATE TO WORKER POOL: Parallel PQC & Sandboxing (Includes PII Shield)
|
|
471
|
-
const preflightReason = this.runPreflightPolicy(name, logic, policy);
|
|
472
|
-
if (preflightReason) {
|
|
473
|
-
stats.failures++;
|
|
474
|
-
stats.lastAttempt = now;
|
|
475
|
-
this.connectionStats.set(clientId, stats);
|
|
476
|
-
return {
|
|
477
|
-
content: [{ type: "text", text: preflightReason }],
|
|
478
|
-
isError: true,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
const result = await this.executeInWorkerPool(args, logic, name);
|
|
482
|
-
if (!result.isError) {
|
|
483
|
-
this.connectionStats.set(clientId, {
|
|
484
|
-
failures: 0,
|
|
485
|
-
lastAttempt: now,
|
|
486
|
-
});
|
|
487
|
-
this.logicCache.set(payloadHash, {
|
|
488
|
-
hash: payloadHash,
|
|
489
|
-
timestamp: now,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
stats.failures++;
|
|
494
|
-
stats.lastAttempt = now;
|
|
495
|
-
this.connectionStats.set(clientId, stats);
|
|
496
|
-
}
|
|
497
|
-
return result;
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
const e = error;
|
|
501
|
-
stats.failures++;
|
|
502
|
-
stats.lastAttempt = now;
|
|
503
|
-
this.connectionStats.set(clientId, stats);
|
|
504
|
-
return {
|
|
505
|
-
content: [
|
|
506
|
-
{ type: "text", text: `ExecutionRuntimeException: ${e.message}` },
|
|
507
|
-
],
|
|
508
|
-
isError: true,
|
|
509
|
-
};
|
|
510
|
-
}
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
const inputSchema = {
|
|
514
|
-
type: "object",
|
|
515
|
-
properties: generatedSchema.properties || {},
|
|
516
|
-
required: generatedSchema.required,
|
|
517
|
-
};
|
|
518
|
-
this.tools.set(name, {
|
|
519
|
-
tool: { name, description: finalDescription, inputSchema },
|
|
520
|
-
handler: finalHandler,
|
|
521
|
-
schema,
|
|
522
|
-
policy,
|
|
523
|
-
});
|
|
524
|
-
// [LIOP-ALPHA] Auto-announce capability to the Mesh P2P DHT if node is active
|
|
525
|
-
if (this.meshNode) {
|
|
526
|
-
this.meshNode.announceCapability(name).catch((err) => {
|
|
527
|
-
log.info(`[LIOP-Mesh] Failed to auto-announce tool ${name}: ${err.message}`);
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
/**
|
|
532
|
-
* Register a dynamic prompt
|
|
533
|
-
*/
|
|
534
|
-
prompt(name, description, args, handler) {
|
|
535
|
-
if (this.prompts.has(name)) {
|
|
536
|
-
throw new Error(`Prompt already registered: ${name}`);
|
|
537
|
-
}
|
|
538
|
-
this.prompts.set(name, {
|
|
539
|
-
prompt: { name, description, arguments: args },
|
|
540
|
-
handler,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* Enables LIOP Zero-Shot Autonomy by registering the Blind Analyst standard prompt.
|
|
545
|
-
*/
|
|
546
|
-
enableZeroShotAutonomy() {
|
|
547
|
-
this.prompt("liop_blind_analyst", "The official Logic-Injection-on-Origin Protocol system prompt. Instructs the LLM on how to securely inject Logic-on-Origin without violating PII or safety constraints.", [], (_request) => {
|
|
548
|
-
return {
|
|
549
|
-
description: "LIOP Blind Analyst Instructions",
|
|
550
|
-
messages: [
|
|
551
|
-
{
|
|
552
|
-
role: "user",
|
|
553
|
-
content: {
|
|
554
|
-
type: "text",
|
|
555
|
-
text: `You are the "Blind Analyst" operating within the Logic-Injection-on-Origin Protocol (LIOP) ecosystem.
|
|
556
|
-
Your objective is to perform secure Logic-on-Origin injections. You must process remote data without ever requesting its extraction.
|
|
557
|
-
|
|
558
|
-
INDUSTRIAL CONSTRAINTS & PROTOCOL RULES:
|
|
559
|
-
1. DATA PRIVACY: NEVER attempt to export Personally Identifiable Information (PII). The LIOP Egress Shield will block any response containing raw IDs, names, or addresses.
|
|
560
|
-
2. AGGREGATION FIRST: Always prefer returning counts, averages, or anonymized summaries.
|
|
561
|
-
3. PAYLOAD ENCAPSULATION: Your JavaScript payloads MUST strictly adhere to the Compact Envelope. DO NOT include markdown backticks or leading text inside the 'payload' argument.
|
|
562
|
-
Structure:
|
|
563
|
-
@LIOP{wasi_v1,AnalysisTask}
|
|
564
|
-
// Your JS Code Here
|
|
565
|
-
@END
|
|
566
|
-
4. RUNTIME SCOPE: The execution environment provides a global 'env' object. Use 'env.records' to access the target dataset.
|
|
567
|
-
5. LOCALIZATION: Format all JSON response keys in the language used by the user in their query (e.g., use Spanish keys if the query is in Spanish).
|
|
568
|
-
6. SCHEMA RIGIDITY: Only use fields defined in the 'Data Dictionary'. Usage of non-existent fields will trigger a sandbox runtime exception.${this.activeSchema
|
|
569
|
-
? `\n\nCURRENT DATA DICTIONARY (STRICT):\n${JSON.stringify(this.activeSchema, null, 2)}`
|
|
570
|
-
: ""}
|
|
571
|
-
|
|
572
|
-
Protocol Adherence is mandatory for successful execution.`,
|
|
573
|
-
},
|
|
574
|
-
},
|
|
575
|
-
],
|
|
576
|
-
};
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Register a dynamic resource
|
|
581
|
-
*/
|
|
582
|
-
resource(name, uri, description, mimeType, content) {
|
|
583
|
-
if (this.resources.has(uri)) {
|
|
584
|
-
throw new Error(`Resource URI already registered: ${uri}`);
|
|
585
|
-
}
|
|
586
|
-
this.resources.set(uri, { name, uri, description, mimeType, content });
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Broadcasts the Data Dictionary to the LLM prior to code injection.
|
|
590
|
-
*/
|
|
591
|
-
dataDictionary(schema, name = "Global Medical Data Dictionary", uri = "liop://schema/global", description = "Exposes the internal database schema for Zero-Shot Autonomy planning") {
|
|
592
|
-
this.activeSchema = schema;
|
|
593
|
-
// [Token Economy] Retroactively update tool descriptions with schema field references.
|
|
594
|
-
// Extracts actual data property names from the JSON Schema structure.
|
|
595
|
-
const schemaDigest = this.extractSchemaFieldSummary(schema);
|
|
596
|
-
for (const [toolName, entry] of this.tools.entries()) {
|
|
597
|
-
if (entry.schema.shape.payload &&
|
|
598
|
-
entry.schema.shape.payload instanceof z.ZodString &&
|
|
599
|
-
entry.tool.description &&
|
|
600
|
-
!entry.tool.description.includes("Data structure:")) {
|
|
601
|
-
entry.tool.description += `\nData structure: ${schemaDigest}. Full schema: resource ${uri}`;
|
|
602
|
-
this.tools.set(toolName, entry);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
this.resource(name, uri, description, "application/json", JSON.stringify(schema, null, 2));
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Manually invalidates the AST Logic Cache (e.g. for Zero-Day patches).
|
|
609
|
-
*/
|
|
610
|
-
clearAstCache() {
|
|
611
|
-
this.logicCache.clear();
|
|
612
|
-
log.info("[LIOP-SDK] AST Security Cache cleared by Admin.");
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Sliding window rate limiter for tool call frequency.
|
|
616
|
-
* Prevents micro-query exfiltration attacks where an attacker
|
|
617
|
-
* makes hundreds of individually-legitimate calls to reconstruct
|
|
618
|
-
* the full dataset field by field. (OWASP A01)
|
|
619
|
-
*/
|
|
620
|
-
checkToolCallRateLimit(toolName) {
|
|
621
|
-
const now = Date.now();
|
|
622
|
-
const windowMs = this.toolCallWindowMs;
|
|
623
|
-
const maxPerWindow = this.toolCallMaxPerWindow;
|
|
624
|
-
const window = this.toolCallWindows.get(toolName) || [];
|
|
625
|
-
// Evict expired timestamps outside the sliding window
|
|
626
|
-
const active = window.filter((t) => now - t < windowMs);
|
|
627
|
-
if (active.length >= maxPerWindow) {
|
|
628
|
-
const retryAfterSec = Math.ceil((active[0] + windowMs - now) / 1000);
|
|
629
|
-
return {
|
|
630
|
-
content: [
|
|
631
|
-
{
|
|
632
|
-
type: "text",
|
|
633
|
-
text: `LIOP_RATE_LIMITED: Too many calls to ${toolName}. ` +
|
|
634
|
-
`Max ${maxPerWindow} per ${windowMs / 1000}s window. ` +
|
|
635
|
-
`Retry after ${retryAfterSec}s.`,
|
|
636
|
-
},
|
|
637
|
-
],
|
|
638
|
-
isError: true,
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
active.push(now);
|
|
642
|
-
this.toolCallWindows.set(toolName, active);
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Emulates calling a tool (used locally or via LIOPMcpBridge)
|
|
647
|
-
*/
|
|
648
|
-
async callTool(request) {
|
|
649
|
-
const entry = this.tools.get(request.name);
|
|
650
|
-
if (!entry) {
|
|
651
|
-
throw new Error(`Tool not found: ${request.name}`);
|
|
652
|
-
}
|
|
653
|
-
// [OWASP-A01] Rate limiting: prevent micro-query exfiltration
|
|
654
|
-
const rateLimitResult = this.checkToolCallRateLimit(request.name);
|
|
655
|
-
if (rateLimitResult)
|
|
656
|
-
return rateLimitResult;
|
|
657
|
-
try {
|
|
658
|
-
// Validate inputs natively with Zod before execution
|
|
659
|
-
const parsedArgs = entry.schema.parse(request.arguments || {});
|
|
660
|
-
// Re-inject the bypass flag if present since Zod might strip unrecognized keys
|
|
661
|
-
if (request.arguments
|
|
662
|
-
?.__liop_bypass_ast_cache === true) {
|
|
663
|
-
parsedArgs.__liop_bypass_ast_cache = true;
|
|
664
|
-
}
|
|
665
|
-
// [LOGIC-ON-ORIGIN] Intercept code injection directly
|
|
666
|
-
if (parsedArgs &&
|
|
667
|
-
typeof parsedArgs.payload === "string") {
|
|
668
|
-
const payload = parsedArgs
|
|
669
|
-
.payload;
|
|
670
|
-
const logic = this.extractLogic(payload);
|
|
671
|
-
if (logic) {
|
|
672
|
-
const preflightReason = this.runPreflightPolicy(request.name, logic, entry.policy);
|
|
673
|
-
if (preflightReason) {
|
|
674
|
-
return {
|
|
675
|
-
content: [{ type: "text", text: preflightReason }],
|
|
676
|
-
isError: true,
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
parsedArgs.payload = logic;
|
|
680
|
-
return await this.executeInWorkerPool(parsedArgs, logic, request.name);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
const result = await entry.handler(parsedArgs, {});
|
|
684
|
-
return result;
|
|
685
|
-
}
|
|
686
|
-
catch (error) {
|
|
687
|
-
const e = error;
|
|
688
|
-
if (e instanceof z.ZodError) {
|
|
689
|
-
return {
|
|
690
|
-
content: [{ type: "text", text: `Validation Error: ${e.message}` }],
|
|
691
|
-
isError: true,
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
return {
|
|
695
|
-
content: [
|
|
696
|
-
{ type: "text", text: `Internal Execution Error: ${e.message}` },
|
|
697
|
-
],
|
|
698
|
-
isError: true,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Retrieves registered tools
|
|
704
|
-
*/
|
|
705
|
-
listTools() {
|
|
706
|
-
return Array.from(this.tools.values()).map((t) => t.tool);
|
|
707
|
-
}
|
|
708
|
-
/**
|
|
709
|
-
* Retrieves registered prompts
|
|
710
|
-
*/
|
|
711
|
-
listPrompts() {
|
|
712
|
-
return Array.from(this.prompts.values()).map((p) => p.prompt);
|
|
713
|
-
}
|
|
714
|
-
/**
|
|
715
|
-
* Gets a specific prompt by name
|
|
716
|
-
*/
|
|
717
|
-
async getPrompt(request) {
|
|
718
|
-
const entry = this.prompts.get(request.name);
|
|
719
|
-
if (!entry) {
|
|
720
|
-
throw new Error(`Prompt not found: ${request.name}`);
|
|
721
|
-
}
|
|
722
|
-
return await entry.handler(request);
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Retrieves registered resources
|
|
726
|
-
*/
|
|
727
|
-
listResources() {
|
|
728
|
-
return Array.from(this.resources.values());
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Reads a specific resource by URI
|
|
732
|
-
*/
|
|
733
|
-
async readResource(uri) {
|
|
734
|
-
const resource = this.resources.get(uri);
|
|
735
|
-
if (!resource) {
|
|
736
|
-
throw new Error(`Resource not found: ${uri}`);
|
|
737
|
-
}
|
|
738
|
-
let text = "No description provided";
|
|
739
|
-
if (typeof resource.content === "function") {
|
|
740
|
-
text = await resource.content();
|
|
741
|
-
}
|
|
742
|
-
else if (typeof resource.content === "string") {
|
|
743
|
-
text = resource.content;
|
|
744
|
-
}
|
|
745
|
-
else if (resource.description) {
|
|
746
|
-
text = resource.description;
|
|
747
|
-
}
|
|
748
|
-
return {
|
|
749
|
-
contents: [
|
|
750
|
-
{
|
|
751
|
-
uri: resource.uri,
|
|
752
|
-
mimeType: resource.mimeType || "text/plain",
|
|
753
|
-
text,
|
|
754
|
-
},
|
|
755
|
-
],
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
getServerInfo() {
|
|
759
|
-
return this.serverInfo;
|
|
760
|
-
}
|
|
761
|
-
getMeshNode() {
|
|
762
|
-
return this.meshNode;
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Injects data into the secure sandbox context for Logic-on-Origin tools.
|
|
766
|
-
*/
|
|
767
|
-
setSandboxData(records) {
|
|
768
|
-
this.sandboxRecords = records;
|
|
769
|
-
}
|
|
770
|
-
getBoundPort() {
|
|
771
|
-
return this.boundPort;
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Connects to the libp2p Kademlia DHT and announces capabilities.
|
|
775
|
-
* Boots the gRPC server for secure Logic-on-Origin.
|
|
776
|
-
*/
|
|
777
|
-
async connectToMesh(options = {}) {
|
|
778
|
-
const envPort = process.env.LIOP_GRPC_PORT
|
|
779
|
-
? Number.parseInt(process.env.LIOP_GRPC_PORT, 10)
|
|
780
|
-
: undefined;
|
|
781
|
-
const port = options.port ?? envPort ?? 50051;
|
|
782
|
-
// 1. Initialize Mesh Node (Discovery)
|
|
783
|
-
this.meshNode = new MeshNode(options.meshConfig);
|
|
784
|
-
await this.meshNode.start();
|
|
785
|
-
// 2. Register LIOP Manifest Protocol Handler
|
|
786
|
-
// This allows remote peers to query our tool/resource metadata dynamically.
|
|
787
|
-
const meshNodeRef = this.meshNode;
|
|
788
|
-
this.meshNode.registerManifestHandler(() => {
|
|
789
|
-
const tools = this.listTools().map((t) => ({
|
|
790
|
-
name: t.name,
|
|
791
|
-
description: t.description,
|
|
792
|
-
inputSchema: t.inputSchema,
|
|
793
|
-
}));
|
|
794
|
-
const resources = Array.from(this.resources.values()).map((r) => ({
|
|
795
|
-
name: r.name,
|
|
796
|
-
uri: r.uri,
|
|
797
|
-
description: r.description,
|
|
798
|
-
mimeType: r.mimeType,
|
|
799
|
-
text: typeof r.content === "string" ? r.content : r.description,
|
|
800
|
-
}));
|
|
801
|
-
return {
|
|
802
|
-
peerId: meshNodeRef.getPeerId(),
|
|
803
|
-
grpcPort: port,
|
|
804
|
-
tools,
|
|
805
|
-
resources,
|
|
806
|
-
serverInfo: this.serverInfo,
|
|
807
|
-
};
|
|
808
|
-
});
|
|
809
|
-
// 3. Announce local tools to the DHT
|
|
810
|
-
for (const tool of this.listTools()) {
|
|
811
|
-
await this.meshNode.announceCapability(tool.name).catch(log.info);
|
|
812
|
-
}
|
|
813
|
-
// 4. Announce manifest availability
|
|
814
|
-
await this.meshNode.announceManifest().catch(log.info);
|
|
815
|
-
// 5. Initialize gRPC Server (Execution)
|
|
816
|
-
this.rpcServer = new LiopRpcServer();
|
|
817
|
-
this.rpcServer.addService({
|
|
818
|
-
negotiateIntent: (call, callback) => {
|
|
819
|
-
const request = call.request;
|
|
820
|
-
log.info(`[LIOP-RPC] Negotiating intent for capability: ${request.capability_hash}`);
|
|
821
|
-
// Standard dynamic import to avoid potential circularity
|
|
822
|
-
import("../rpc/crypto/kyber.js").then(async ({ Kyber768Wrapper }) => {
|
|
823
|
-
const { publicKey, secretKey } = await Kyber768Wrapper.generateKeyPair();
|
|
824
|
-
const sessionToken = crypto.randomUUID();
|
|
825
|
-
this.sessions.set(sessionToken, {
|
|
826
|
-
capability_hash: request.capability_hash,
|
|
827
|
-
kyber_sk: secretKey,
|
|
828
|
-
});
|
|
829
|
-
callback(null, {
|
|
830
|
-
accepted: true,
|
|
831
|
-
session_token: sessionToken,
|
|
832
|
-
error_message: "",
|
|
833
|
-
kyber_public_key: publicKey,
|
|
834
|
-
});
|
|
835
|
-
});
|
|
836
|
-
},
|
|
837
|
-
executeLogic: async (call) => {
|
|
838
|
-
const request = call.request;
|
|
839
|
-
log.info(`[LIOP-RPC] Executing Logic-on-Origin for session: ${request.session_token}`);
|
|
840
|
-
const session = this.sessions.get(request.session_token);
|
|
841
|
-
if (!session) {
|
|
842
|
-
call.emit("error", {
|
|
843
|
-
code: grpc.status.UNAUTHENTICATED,
|
|
844
|
-
details: "Invalid session token",
|
|
845
|
-
});
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
try {
|
|
849
|
-
// Pass to Worker Pool for PQC Decryption and WASI/V8 execution
|
|
850
|
-
const workerResponse = await this.workerPool.run({
|
|
851
|
-
ciphertext: request.pqc_ciphertext,
|
|
852
|
-
secretKeyObj: Array.from(session.kyber_sk),
|
|
853
|
-
wasmBinary: request.wasm_binary,
|
|
854
|
-
inputs: request.inputs,
|
|
855
|
-
aesNonce: request.aes_nonce,
|
|
856
|
-
records: this.sandboxRecords,
|
|
857
|
-
sessionToken: request.session_token,
|
|
858
|
-
isEncrypted: true,
|
|
859
|
-
});
|
|
860
|
-
let finalOutput;
|
|
861
|
-
try {
|
|
862
|
-
finalOutput =
|
|
863
|
-
typeof workerResponse.output === "string"
|
|
864
|
-
? workerResponse.output
|
|
865
|
-
: JSON.stringify(workerResponse.output);
|
|
866
|
-
// [PROTOCOL TRANSFORMER] Support for Proxied Tool Calls
|
|
867
|
-
const decoded = JSON.parse(finalOutput);
|
|
868
|
-
if (decoded.__liop_proxy_tool) {
|
|
869
|
-
log.info(`[LIOP-RPC] Executing Proxied Tool: ${decoded.__liop_proxy_tool}`);
|
|
870
|
-
const toolResult = await this.callTool({
|
|
871
|
-
name: decoded.__liop_proxy_tool,
|
|
872
|
-
arguments: decoded.__liop_proxy_args || {},
|
|
873
|
-
});
|
|
874
|
-
finalOutput = JSON.stringify(toolResult);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
catch {
|
|
878
|
-
finalOutput = String(workerResponse.output);
|
|
879
|
-
}
|
|
880
|
-
const response = {
|
|
881
|
-
semantic_evidence: finalOutput,
|
|
882
|
-
cryptographic_proof: Buffer.from(workerResponse.image_id || "", "hex"),
|
|
883
|
-
zk_receipt: workerResponse.zk_receipt
|
|
884
|
-
? Buffer.from(workerResponse.zk_receipt, "base64")
|
|
885
|
-
: Buffer.from(""),
|
|
886
|
-
is_error: false,
|
|
887
|
-
};
|
|
888
|
-
// Final PII check for gRPC egress
|
|
889
|
-
const violation = this.piiScanner.scan([
|
|
890
|
-
{ type: "text", text: finalOutput },
|
|
891
|
-
]);
|
|
892
|
-
const aggregationViolation = this.violatesAggregationFirstPolicy(this.unwrapForAggregationPolicyScan(finalOutput));
|
|
893
|
-
if (violation || aggregationViolation) {
|
|
894
|
-
// SEC-CRITICAL: Log details server-side, never expose to caller
|
|
895
|
-
const internalReason = violation || "Aggregation-First Policy Violation";
|
|
896
|
-
log.info(`[LIOP-RPC] Secure egress blocked in gRPC stream: ${internalReason}`);
|
|
897
|
-
response.semantic_evidence =
|
|
898
|
-
"[LIOP] Egress Security Violation. Output blocked due to policy enforcement.";
|
|
899
|
-
response.is_error = true;
|
|
900
|
-
}
|
|
901
|
-
call.write(response, () => {
|
|
902
|
-
call.end();
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
catch (error) {
|
|
906
|
-
const e = error;
|
|
907
|
-
const isDev = process.env.NODE_ENV === "development" ||
|
|
908
|
-
process.env.NODE_ENV === "test";
|
|
909
|
-
const detail = e.message || String(error);
|
|
910
|
-
log.error(`[LIOP-RPC] Execution Error: ${detail}`);
|
|
911
|
-
const errorMessage = isDev
|
|
912
|
-
? `Execution Error: ${detail}`
|
|
913
|
-
: "[LIOP] Execution Failed. The injected logic violated runtime constraints or encountered a fatal error.";
|
|
914
|
-
// Send error response before closing, avoiding "stream closed without results"
|
|
915
|
-
const errorResponse = {
|
|
916
|
-
semantic_evidence: errorMessage,
|
|
917
|
-
cryptographic_proof: Buffer.from(""),
|
|
918
|
-
zk_receipt: Buffer.from(""),
|
|
919
|
-
is_error: true,
|
|
920
|
-
};
|
|
921
|
-
try {
|
|
922
|
-
call.write(errorResponse, () => {
|
|
923
|
-
call.end();
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
catch (_writeErr) {
|
|
927
|
-
call.end();
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
},
|
|
931
|
-
});
|
|
932
|
-
this.boundPort = await this.rpcServer.listen(port);
|
|
933
|
-
log.info(`[LIOP-SDK] Node successfully announced to Mesh. PeerID: ${this.meshNode.getPeerId()}`);
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Internal worker execution with Egress Filtering logic.
|
|
937
|
-
*/
|
|
938
|
-
async executeInWorkerPool(_args, rawPayload, toolName) {
|
|
939
|
-
try {
|
|
940
|
-
// Transparent local execution without dynamic PQC
|
|
941
|
-
const workerResponse = await this.workerPool.run({
|
|
942
|
-
ciphertext: new Uint8Array(0),
|
|
943
|
-
secretKeyObj: Array.from(new Uint8Array(0)),
|
|
944
|
-
kyberPublicKey: new Uint8Array(0),
|
|
945
|
-
wasmBinary: Buffer.from(rawPayload),
|
|
946
|
-
inputs: {},
|
|
947
|
-
records: this.sandboxRecords,
|
|
948
|
-
sessionToken: "local-dev-token",
|
|
949
|
-
isEncrypted: false, // Use plaintext for local Logic-on-Origin injection
|
|
950
|
-
});
|
|
951
|
-
// Standard MCP Content Array
|
|
952
|
-
const textOutput = JSON.stringify({
|
|
953
|
-
computation_result: workerResponse.output,
|
|
954
|
-
image_id: workerResponse.image_id,
|
|
955
|
-
zk_receipt: workerResponse.zk_receipt,
|
|
956
|
-
status: "Worker Pool Execution Success",
|
|
957
|
-
});
|
|
958
|
-
const content = [
|
|
959
|
-
{
|
|
960
|
-
type: "text",
|
|
961
|
-
text: textOutput,
|
|
962
|
-
},
|
|
963
|
-
];
|
|
964
|
-
const toolPolicy = toolName
|
|
965
|
-
? this.tools.get(toolName)?.policy
|
|
966
|
-
: undefined;
|
|
967
|
-
const policyViolation = this.validateOutputPolicy(toolName || "unknown_tool", workerResponse.output, toolPolicy);
|
|
968
|
-
if (policyViolation) {
|
|
969
|
-
// SEC-CRITICAL: Log details server-side, never expose to caller in Production
|
|
970
|
-
log.info(`[LIOP-SDK] Output policy blocked for ${toolName || "unknown_tool"}: ${policyViolation}`);
|
|
971
|
-
const isDev = process.env.NODE_ENV === "development" ||
|
|
972
|
-
process.env.NODE_ENV === "test";
|
|
973
|
-
const errorMessage = isDev
|
|
974
|
-
? policyViolation
|
|
975
|
-
: "[LIOP] Egress Security Violation. Output blocked due to policy enforcement. HINT: Return only aggregated, non-PII results using .reduce() to produce a flat {key:value} object with allowed schema fields.";
|
|
976
|
-
return {
|
|
977
|
-
content: [
|
|
978
|
-
{
|
|
979
|
-
type: "text",
|
|
980
|
-
text: errorMessage,
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
isError: true,
|
|
984
|
-
};
|
|
985
|
-
}
|
|
986
|
-
// Professional PII Protection Guard
|
|
987
|
-
const violation = this.piiScanner.scan(content);
|
|
988
|
-
const aggregationViolation = this.violatesAggregationFirstPolicy(workerResponse.output);
|
|
989
|
-
if (violation || aggregationViolation) {
|
|
990
|
-
// SEC-CRITICAL: Log the specific violation reason server-side only.
|
|
991
|
-
// Never expose detection details (entity names, matched values) to the caller in Production.
|
|
992
|
-
const internalReason = violation ||
|
|
993
|
-
"Aggregation-First Policy Violation: Output blocked due to dynamic flat-key policy enforcement.";
|
|
994
|
-
log.info(`[LIOP-SDK] Secure egress blocked in local execution: ${internalReason}`);
|
|
995
|
-
const isDev = process.env.NODE_ENV === "development" ||
|
|
996
|
-
process.env.NODE_ENV === "test";
|
|
997
|
-
const errorMessage = isDev
|
|
998
|
-
? `[LIOP] Egress Security Violation: ${internalReason}`
|
|
999
|
-
: "[LIOP] Egress Security Violation. Output blocked due to policy enforcement. HINT: Return only aggregated, non-PII results using .reduce() to produce a flat {key:value} object with allowed schema fields.";
|
|
1000
|
-
return {
|
|
1001
|
-
content: [
|
|
1002
|
-
{
|
|
1003
|
-
type: "text",
|
|
1004
|
-
text: errorMessage,
|
|
1005
|
-
},
|
|
1006
|
-
],
|
|
1007
|
-
isError: true,
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
return { content };
|
|
1011
|
-
}
|
|
1012
|
-
catch (error) {
|
|
1013
|
-
const e = error;
|
|
1014
|
-
const isDev = process.env.NODE_ENV === "development" ||
|
|
1015
|
-
process.env.NODE_ENV === "test";
|
|
1016
|
-
const detail = e.message || String(error);
|
|
1017
|
-
log.error(`[LIOP-SDK] WorkerPool Execution Fault: ${detail}`);
|
|
1018
|
-
const errorMessage = isDev
|
|
1019
|
-
? `WorkerPoolError: ${detail}`
|
|
1020
|
-
: "[LIOP] Execution Failed. The injected logic violated runtime constraints or encountered a fatal error.";
|
|
1021
|
-
return {
|
|
1022
|
-
content: [
|
|
1023
|
-
{
|
|
1024
|
-
type: "text",
|
|
1025
|
-
text: errorMessage,
|
|
1026
|
-
},
|
|
1027
|
-
],
|
|
1028
|
-
isError: true,
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
/**
|
|
1033
|
-
* Safely destroys the worker pool, gRPC server, and Mesh node.
|
|
1034
|
-
* Recommended to be called during graceful shutdowns or test teardowns.
|
|
1035
|
-
*/
|
|
1036
|
-
async close() {
|
|
1037
|
-
if (this.workerPool) {
|
|
1038
|
-
await this.workerPool.close({ force: true });
|
|
1039
|
-
}
|
|
1040
|
-
if (this.rpcServer) {
|
|
1041
|
-
await this.rpcServer.stop();
|
|
1042
|
-
}
|
|
1043
|
-
if (this.meshNode) {
|
|
1044
|
-
await this.meshNode.stop();
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|