@solongate/proxy 0.1.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 +136 -0
- package/dist/index.js +1852 -0
- package/dist/init.js +275 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1852 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { readFileSync, existsSync } from "fs";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
var PRESETS = {
|
|
7
|
+
restricted: {
|
|
8
|
+
id: "restricted",
|
|
9
|
+
name: "Restricted",
|
|
10
|
+
description: "Blocks dangerous tools (shell, web), allows safe tools",
|
|
11
|
+
version: 1,
|
|
12
|
+
rules: [
|
|
13
|
+
{
|
|
14
|
+
id: "deny-shell",
|
|
15
|
+
description: "Block shell execution",
|
|
16
|
+
effect: "DENY",
|
|
17
|
+
priority: 100,
|
|
18
|
+
toolPattern: "*shell*",
|
|
19
|
+
permission: "EXECUTE",
|
|
20
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
21
|
+
enabled: true,
|
|
22
|
+
createdAt: "",
|
|
23
|
+
updatedAt: ""
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "deny-exec",
|
|
27
|
+
description: "Block command execution",
|
|
28
|
+
effect: "DENY",
|
|
29
|
+
priority: 101,
|
|
30
|
+
toolPattern: "*exec*",
|
|
31
|
+
permission: "EXECUTE",
|
|
32
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
33
|
+
enabled: true,
|
|
34
|
+
createdAt: "",
|
|
35
|
+
updatedAt: ""
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "deny-eval",
|
|
39
|
+
description: "Block code eval",
|
|
40
|
+
effect: "DENY",
|
|
41
|
+
priority: 102,
|
|
42
|
+
toolPattern: "*eval*",
|
|
43
|
+
permission: "EXECUTE",
|
|
44
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
45
|
+
enabled: true,
|
|
46
|
+
createdAt: "",
|
|
47
|
+
updatedAt: ""
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "allow-rest",
|
|
51
|
+
description: "Allow all other tools",
|
|
52
|
+
effect: "ALLOW",
|
|
53
|
+
priority: 1e3,
|
|
54
|
+
toolPattern: "*",
|
|
55
|
+
permission: "EXECUTE",
|
|
56
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
57
|
+
enabled: true,
|
|
58
|
+
createdAt: "",
|
|
59
|
+
updatedAt: ""
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
createdAt: "",
|
|
63
|
+
updatedAt: ""
|
|
64
|
+
},
|
|
65
|
+
"read-only": {
|
|
66
|
+
id: "read-only",
|
|
67
|
+
name: "Read Only",
|
|
68
|
+
description: "Only allows read operations, blocks writes and execution",
|
|
69
|
+
version: 1,
|
|
70
|
+
rules: [
|
|
71
|
+
{
|
|
72
|
+
id: "allow-read",
|
|
73
|
+
description: "Allow read tools",
|
|
74
|
+
effect: "ALLOW",
|
|
75
|
+
priority: 100,
|
|
76
|
+
toolPattern: "*read*",
|
|
77
|
+
permission: "EXECUTE",
|
|
78
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
79
|
+
enabled: true,
|
|
80
|
+
createdAt: "",
|
|
81
|
+
updatedAt: ""
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "allow-list",
|
|
85
|
+
description: "Allow list tools",
|
|
86
|
+
effect: "ALLOW",
|
|
87
|
+
priority: 101,
|
|
88
|
+
toolPattern: "*list*",
|
|
89
|
+
permission: "EXECUTE",
|
|
90
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
91
|
+
enabled: true,
|
|
92
|
+
createdAt: "",
|
|
93
|
+
updatedAt: ""
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "allow-get",
|
|
97
|
+
description: "Allow get tools",
|
|
98
|
+
effect: "ALLOW",
|
|
99
|
+
priority: 102,
|
|
100
|
+
toolPattern: "*get*",
|
|
101
|
+
permission: "EXECUTE",
|
|
102
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
103
|
+
enabled: true,
|
|
104
|
+
createdAt: "",
|
|
105
|
+
updatedAt: ""
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "allow-search",
|
|
109
|
+
description: "Allow search tools",
|
|
110
|
+
effect: "ALLOW",
|
|
111
|
+
priority: 103,
|
|
112
|
+
toolPattern: "*search*",
|
|
113
|
+
permission: "EXECUTE",
|
|
114
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
115
|
+
enabled: true,
|
|
116
|
+
createdAt: "",
|
|
117
|
+
updatedAt: ""
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "allow-query",
|
|
121
|
+
description: "Allow query tools",
|
|
122
|
+
effect: "ALLOW",
|
|
123
|
+
priority: 104,
|
|
124
|
+
toolPattern: "*query*",
|
|
125
|
+
permission: "EXECUTE",
|
|
126
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
127
|
+
enabled: true,
|
|
128
|
+
createdAt: "",
|
|
129
|
+
updatedAt: ""
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
createdAt: "",
|
|
133
|
+
updatedAt: ""
|
|
134
|
+
},
|
|
135
|
+
permissive: {
|
|
136
|
+
id: "permissive",
|
|
137
|
+
name: "Permissive",
|
|
138
|
+
description: "Allows all tool calls (monitoring only)",
|
|
139
|
+
version: 1,
|
|
140
|
+
rules: [
|
|
141
|
+
{
|
|
142
|
+
id: "allow-all",
|
|
143
|
+
description: "Allow all",
|
|
144
|
+
effect: "ALLOW",
|
|
145
|
+
priority: 1e3,
|
|
146
|
+
toolPattern: "*",
|
|
147
|
+
permission: "EXECUTE",
|
|
148
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
149
|
+
enabled: true,
|
|
150
|
+
createdAt: "",
|
|
151
|
+
updatedAt: ""
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
createdAt: "",
|
|
155
|
+
updatedAt: ""
|
|
156
|
+
},
|
|
157
|
+
"deny-all": {
|
|
158
|
+
id: "deny-all",
|
|
159
|
+
name: "Deny All",
|
|
160
|
+
description: "Blocks all tool calls",
|
|
161
|
+
version: 1,
|
|
162
|
+
rules: [],
|
|
163
|
+
createdAt: "",
|
|
164
|
+
updatedAt: ""
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
function loadPolicy(source) {
|
|
168
|
+
if (typeof source === "object") return source;
|
|
169
|
+
if (PRESETS[source]) return PRESETS[source];
|
|
170
|
+
const filePath = resolve(source);
|
|
171
|
+
if (existsSync(filePath)) {
|
|
172
|
+
const content = readFileSync(filePath, "utf-8");
|
|
173
|
+
return JSON.parse(content);
|
|
174
|
+
}
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function parseArgs(argv) {
|
|
180
|
+
const args = argv.slice(2);
|
|
181
|
+
let policySource = "restricted";
|
|
182
|
+
let name = "solongate-proxy";
|
|
183
|
+
let verbose = false;
|
|
184
|
+
let validateInput = true;
|
|
185
|
+
let rateLimitPerTool;
|
|
186
|
+
let globalRateLimit;
|
|
187
|
+
let configFile;
|
|
188
|
+
let separatorIndex = args.indexOf("--");
|
|
189
|
+
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
190
|
+
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
191
|
+
for (let i = 0; i < flags.length; i++) {
|
|
192
|
+
switch (flags[i]) {
|
|
193
|
+
case "--policy":
|
|
194
|
+
policySource = flags[++i];
|
|
195
|
+
break;
|
|
196
|
+
case "--name":
|
|
197
|
+
name = flags[++i];
|
|
198
|
+
break;
|
|
199
|
+
case "--verbose":
|
|
200
|
+
verbose = true;
|
|
201
|
+
break;
|
|
202
|
+
case "--no-input-guard":
|
|
203
|
+
validateInput = false;
|
|
204
|
+
break;
|
|
205
|
+
case "--rate-limit":
|
|
206
|
+
rateLimitPerTool = parseInt(flags[++i], 10);
|
|
207
|
+
break;
|
|
208
|
+
case "--global-rate-limit":
|
|
209
|
+
globalRateLimit = parseInt(flags[++i], 10);
|
|
210
|
+
break;
|
|
211
|
+
case "--config":
|
|
212
|
+
configFile = flags[++i];
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (configFile) {
|
|
217
|
+
const filePath = resolve(configFile);
|
|
218
|
+
const content = readFileSync(filePath, "utf-8");
|
|
219
|
+
const fileConfig = JSON.parse(content);
|
|
220
|
+
if (!fileConfig.upstream) {
|
|
221
|
+
throw new Error('Config file must include "upstream" with at least "command"');
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
upstream: fileConfig.upstream,
|
|
225
|
+
policy: loadPolicy(fileConfig.policy ?? policySource),
|
|
226
|
+
name: fileConfig.name ?? name,
|
|
227
|
+
verbose: fileConfig.verbose ?? verbose,
|
|
228
|
+
validateInput: fileConfig.validateInput ?? validateInput,
|
|
229
|
+
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
230
|
+
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (upstreamArgs.length === 0) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --config solongate.json\n"
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const [command, ...commandArgs] = upstreamArgs;
|
|
239
|
+
return {
|
|
240
|
+
upstream: {
|
|
241
|
+
command,
|
|
242
|
+
args: commandArgs,
|
|
243
|
+
env: { ...process.env }
|
|
244
|
+
},
|
|
245
|
+
policy: loadPolicy(policySource),
|
|
246
|
+
name,
|
|
247
|
+
verbose,
|
|
248
|
+
validateInput,
|
|
249
|
+
rateLimitPerTool,
|
|
250
|
+
globalRateLimit
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/proxy.ts
|
|
255
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
256
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
257
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
258
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
259
|
+
import {
|
|
260
|
+
ListToolsRequestSchema,
|
|
261
|
+
CallToolRequestSchema,
|
|
262
|
+
ListResourcesRequestSchema,
|
|
263
|
+
ListPromptsRequestSchema,
|
|
264
|
+
GetPromptRequestSchema,
|
|
265
|
+
ReadResourceRequestSchema,
|
|
266
|
+
ListResourceTemplatesRequestSchema
|
|
267
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
268
|
+
|
|
269
|
+
// ../core/dist/index.js
|
|
270
|
+
import { z } from "zod";
|
|
271
|
+
var SolonGateError = class extends Error {
|
|
272
|
+
code;
|
|
273
|
+
timestamp;
|
|
274
|
+
details;
|
|
275
|
+
constructor(message, code, details = {}) {
|
|
276
|
+
super(message);
|
|
277
|
+
this.name = "SolonGateError";
|
|
278
|
+
this.code = code;
|
|
279
|
+
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
280
|
+
this.details = Object.freeze({ ...details });
|
|
281
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Serializable representation for logging and API responses.
|
|
285
|
+
* Never includes stack traces (information leakage prevention).
|
|
286
|
+
*/
|
|
287
|
+
toJSON() {
|
|
288
|
+
return {
|
|
289
|
+
name: this.name,
|
|
290
|
+
code: this.code,
|
|
291
|
+
message: this.message,
|
|
292
|
+
timestamp: this.timestamp,
|
|
293
|
+
details: this.details
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var PolicyDeniedError = class extends SolonGateError {
|
|
298
|
+
constructor(toolName, reason, details = {}) {
|
|
299
|
+
super(
|
|
300
|
+
`Policy denied execution of tool "${toolName}": ${reason}`,
|
|
301
|
+
"POLICY_DENIED",
|
|
302
|
+
{ toolName, reason, ...details }
|
|
303
|
+
);
|
|
304
|
+
this.name = "PolicyDeniedError";
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
var SchemaValidationError = class extends SolonGateError {
|
|
308
|
+
constructor(toolName, validationErrors) {
|
|
309
|
+
super(
|
|
310
|
+
`Schema validation failed for tool "${toolName}": ${validationErrors.join("; ")}`,
|
|
311
|
+
"SCHEMA_VALIDATION_FAILED",
|
|
312
|
+
{ toolName, validationErrors }
|
|
313
|
+
);
|
|
314
|
+
this.name = "SchemaValidationError";
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
var RateLimitError = class extends SolonGateError {
|
|
318
|
+
constructor(toolName, limitPerMinute) {
|
|
319
|
+
super(
|
|
320
|
+
`Rate limit exceeded for tool "${toolName}": max ${limitPerMinute}/min`,
|
|
321
|
+
"RATE_LIMIT_EXCEEDED",
|
|
322
|
+
{ toolName, limitPerMinute }
|
|
323
|
+
);
|
|
324
|
+
this.name = "RateLimitError";
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
var TrustLevel = {
|
|
328
|
+
UNTRUSTED: "UNTRUSTED",
|
|
329
|
+
VERIFIED: "VERIFIED",
|
|
330
|
+
TRUSTED: "TRUSTED"
|
|
331
|
+
};
|
|
332
|
+
var Permission = {
|
|
333
|
+
READ: "READ",
|
|
334
|
+
WRITE: "WRITE",
|
|
335
|
+
EXECUTE: "EXECUTE"
|
|
336
|
+
};
|
|
337
|
+
var PermissionSchema = z.enum(["READ", "WRITE", "EXECUTE"]);
|
|
338
|
+
var NO_PERMISSIONS = Object.freeze(
|
|
339
|
+
/* @__PURE__ */ new Set()
|
|
340
|
+
);
|
|
341
|
+
var READ_ONLY = Object.freeze(
|
|
342
|
+
/* @__PURE__ */ new Set([Permission.READ])
|
|
343
|
+
);
|
|
344
|
+
var PolicyEffect = {
|
|
345
|
+
ALLOW: "ALLOW",
|
|
346
|
+
DENY: "DENY"
|
|
347
|
+
};
|
|
348
|
+
var PolicyRuleSchema = z.object({
|
|
349
|
+
id: z.string().min(1).max(256),
|
|
350
|
+
description: z.string().max(1024),
|
|
351
|
+
effect: z.enum(["ALLOW", "DENY"]),
|
|
352
|
+
priority: z.number().int().min(0).max(1e4).default(1e3),
|
|
353
|
+
toolPattern: z.string().min(1).max(512),
|
|
354
|
+
permission: z.enum(["READ", "WRITE", "EXECUTE"]),
|
|
355
|
+
minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
356
|
+
argumentConstraints: z.record(z.unknown()).optional(),
|
|
357
|
+
pathConstraints: z.object({
|
|
358
|
+
allowed: z.array(z.string()).optional(),
|
|
359
|
+
denied: z.array(z.string()).optional(),
|
|
360
|
+
rootDirectory: z.string().optional(),
|
|
361
|
+
allowSymlinks: z.boolean().optional()
|
|
362
|
+
}).optional(),
|
|
363
|
+
enabled: z.boolean().default(true),
|
|
364
|
+
createdAt: z.string().datetime(),
|
|
365
|
+
updatedAt: z.string().datetime()
|
|
366
|
+
});
|
|
367
|
+
var PolicySetSchema = z.object({
|
|
368
|
+
id: z.string().min(1).max(256),
|
|
369
|
+
name: z.string().min(1).max(256),
|
|
370
|
+
description: z.string().max(2048),
|
|
371
|
+
version: z.number().int().min(0),
|
|
372
|
+
rules: z.array(PolicyRuleSchema),
|
|
373
|
+
createdAt: z.string().datetime(),
|
|
374
|
+
updatedAt: z.string().datetime()
|
|
375
|
+
});
|
|
376
|
+
function createSecurityContext(params) {
|
|
377
|
+
return {
|
|
378
|
+
trustLevel: "UNTRUSTED",
|
|
379
|
+
grantedPermissions: /* @__PURE__ */ new Set(),
|
|
380
|
+
sessionId: null,
|
|
381
|
+
metadata: {},
|
|
382
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
383
|
+
...params
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
var DEFAULT_POLICY_EFFECT = "DENY";
|
|
387
|
+
var MAX_RULES_PER_POLICY_SET = 1e3;
|
|
388
|
+
var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
389
|
+
var POLICY_EVALUATION_TIMEOUT_MS = 100;
|
|
390
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
391
|
+
var RATE_LIMIT_MAX_ENTRIES = 1e4;
|
|
392
|
+
var UNSAFE_CONFIGURATION_WARNINGS = {
|
|
393
|
+
WILDCARD_ALLOW: "Wildcard ALLOW rules grant permission to ALL tools. This bypasses the default-deny model.",
|
|
394
|
+
TRUSTED_LEVEL_EXTERNAL: "Setting trust level to TRUSTED for external requests bypasses all security checks.",
|
|
395
|
+
WRITE_WITHOUT_READ: "Granting WRITE without READ is unusual and may indicate a misconfiguration.",
|
|
396
|
+
EXECUTE_WITHOUT_REVIEW: "EXECUTE permission allows tools to perform arbitrary actions. Review carefully.",
|
|
397
|
+
RATE_LIMIT_ZERO: "A rate limit of 0 means unlimited calls. This removes protection against runaway loops.",
|
|
398
|
+
DISABLED_VALIDATION: "Disabling schema validation removes input sanitization protections."
|
|
399
|
+
};
|
|
400
|
+
function createDeniedToolResult(reason) {
|
|
401
|
+
return {
|
|
402
|
+
content: [
|
|
403
|
+
{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: JSON.stringify({
|
|
406
|
+
error: "POLICY_DENIED",
|
|
407
|
+
message: reason,
|
|
408
|
+
hint: "This tool call was blocked by SolonGate security policy. Check your policy configuration."
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
],
|
|
412
|
+
isError: true
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
416
|
+
pathTraversal: true,
|
|
417
|
+
shellInjection: true,
|
|
418
|
+
wildcardAbuse: true,
|
|
419
|
+
lengthLimit: 4096,
|
|
420
|
+
entropyLimit: true
|
|
421
|
+
});
|
|
422
|
+
var PATH_TRAVERSAL_PATTERNS = [
|
|
423
|
+
/\.\.\//,
|
|
424
|
+
// ../
|
|
425
|
+
/\.\.\\/,
|
|
426
|
+
// ..\
|
|
427
|
+
/%2e%2e/i,
|
|
428
|
+
// URL-encoded ..
|
|
429
|
+
/%2e\./i,
|
|
430
|
+
// partial URL-encoded
|
|
431
|
+
/\.%2e/i,
|
|
432
|
+
// partial URL-encoded
|
|
433
|
+
/%252e%252e/i,
|
|
434
|
+
// double URL-encoded
|
|
435
|
+
/\.\.\0/
|
|
436
|
+
// null byte variant
|
|
437
|
+
];
|
|
438
|
+
var SENSITIVE_PATHS = [
|
|
439
|
+
/\/etc\/passwd/i,
|
|
440
|
+
/\/etc\/shadow/i,
|
|
441
|
+
/\/proc\//i,
|
|
442
|
+
/\/dev\//i,
|
|
443
|
+
/c:\\windows\\system32/i,
|
|
444
|
+
/c:\\windows\\syswow64/i,
|
|
445
|
+
/\/root\//i,
|
|
446
|
+
/~\//
|
|
447
|
+
];
|
|
448
|
+
function detectPathTraversal(value) {
|
|
449
|
+
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
450
|
+
if (pattern.test(value)) return true;
|
|
451
|
+
}
|
|
452
|
+
for (const pattern of SENSITIVE_PATHS) {
|
|
453
|
+
if (pattern.test(value)) return true;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
var SHELL_INJECTION_PATTERNS = [
|
|
458
|
+
/[;|&`]/,
|
|
459
|
+
// Command separators and backtick execution
|
|
460
|
+
/\$\(/,
|
|
461
|
+
// Command substitution $(...)
|
|
462
|
+
/\$\{/,
|
|
463
|
+
// Variable expansion ${...}
|
|
464
|
+
/>\s*/,
|
|
465
|
+
// Output redirect
|
|
466
|
+
/<\s*/,
|
|
467
|
+
// Input redirect
|
|
468
|
+
/&&/,
|
|
469
|
+
// AND chaining
|
|
470
|
+
/\|\|/,
|
|
471
|
+
// OR chaining
|
|
472
|
+
/\beval\b/i,
|
|
473
|
+
// eval command
|
|
474
|
+
/\bexec\b/i,
|
|
475
|
+
// exec command
|
|
476
|
+
/\bsystem\b/i
|
|
477
|
+
// system call
|
|
478
|
+
];
|
|
479
|
+
function detectShellInjection(value) {
|
|
480
|
+
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
481
|
+
if (pattern.test(value)) return true;
|
|
482
|
+
}
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
var MAX_WILDCARDS_PER_VALUE = 3;
|
|
486
|
+
function detectWildcardAbuse(value) {
|
|
487
|
+
if (value.includes("**")) return true;
|
|
488
|
+
const wildcardCount = (value.match(/\*/g) || []).length;
|
|
489
|
+
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
function checkLengthLimits(value, maxLength = 4096) {
|
|
493
|
+
return value.length <= maxLength;
|
|
494
|
+
}
|
|
495
|
+
var ENTROPY_THRESHOLD = 4.5;
|
|
496
|
+
var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
|
|
497
|
+
function checkEntropyLimits(value) {
|
|
498
|
+
if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
|
|
499
|
+
const entropy = calculateShannonEntropy(value);
|
|
500
|
+
return entropy <= ENTROPY_THRESHOLD;
|
|
501
|
+
}
|
|
502
|
+
function calculateShannonEntropy(str) {
|
|
503
|
+
const freq = /* @__PURE__ */ new Map();
|
|
504
|
+
for (const char of str) {
|
|
505
|
+
freq.set(char, (freq.get(char) ?? 0) + 1);
|
|
506
|
+
}
|
|
507
|
+
let entropy = 0;
|
|
508
|
+
const len = str.length;
|
|
509
|
+
for (const count of freq.values()) {
|
|
510
|
+
const p = count / len;
|
|
511
|
+
if (p > 0) {
|
|
512
|
+
entropy -= p * Math.log2(p);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return entropy;
|
|
516
|
+
}
|
|
517
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
|
|
518
|
+
const threats = [];
|
|
519
|
+
if (typeof value !== "string") {
|
|
520
|
+
if (typeof value === "object" && value !== null) {
|
|
521
|
+
return sanitizeObject(field, value, config);
|
|
522
|
+
}
|
|
523
|
+
return { safe: true, threats: [] };
|
|
524
|
+
}
|
|
525
|
+
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
526
|
+
threats.push({
|
|
527
|
+
type: "PATH_TRAVERSAL",
|
|
528
|
+
field,
|
|
529
|
+
value: truncate(value, 100),
|
|
530
|
+
description: "Path traversal pattern detected"
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (config.shellInjection && detectShellInjection(value)) {
|
|
534
|
+
threats.push({
|
|
535
|
+
type: "SHELL_INJECTION",
|
|
536
|
+
field,
|
|
537
|
+
value: truncate(value, 100),
|
|
538
|
+
description: "Shell injection pattern detected"
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
542
|
+
threats.push({
|
|
543
|
+
type: "WILDCARD_ABUSE",
|
|
544
|
+
field,
|
|
545
|
+
value: truncate(value, 100),
|
|
546
|
+
description: "Wildcard abuse pattern detected"
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
550
|
+
threats.push({
|
|
551
|
+
type: "LENGTH_EXCEEDED",
|
|
552
|
+
field,
|
|
553
|
+
value: `[${value.length} chars]`,
|
|
554
|
+
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
558
|
+
threats.push({
|
|
559
|
+
type: "HIGH_ENTROPY",
|
|
560
|
+
field,
|
|
561
|
+
value: truncate(value, 100),
|
|
562
|
+
description: "High entropy string detected - possible encoded payload"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return { safe: threats.length === 0, threats };
|
|
566
|
+
}
|
|
567
|
+
function sanitizeObject(basePath, obj, config) {
|
|
568
|
+
const threats = [];
|
|
569
|
+
if (Array.isArray(obj)) {
|
|
570
|
+
for (let i = 0; i < obj.length; i++) {
|
|
571
|
+
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
572
|
+
threats.push(...result.threats);
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
576
|
+
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
577
|
+
threats.push(...result.threats);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { safe: threats.length === 0, threats };
|
|
581
|
+
}
|
|
582
|
+
function truncate(str, maxLen) {
|
|
583
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
584
|
+
}
|
|
585
|
+
var DEFAULT_TOKEN_TTL_SECONDS = 30;
|
|
586
|
+
var TOKEN_ALGORITHM = "HS256";
|
|
587
|
+
var MIN_SECRET_LENGTH = 32;
|
|
588
|
+
|
|
589
|
+
// ../policy-engine/dist/index.js
|
|
590
|
+
import { createHash } from "crypto";
|
|
591
|
+
function normalizePath(path) {
|
|
592
|
+
let normalized = path.replace(/\\/g, "/");
|
|
593
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
594
|
+
normalized = normalized.slice(0, -1);
|
|
595
|
+
}
|
|
596
|
+
const parts = normalized.split("/");
|
|
597
|
+
const resolved = [];
|
|
598
|
+
for (const part of parts) {
|
|
599
|
+
if (part === "." || part === "") {
|
|
600
|
+
if (resolved.length === 0) resolved.push("");
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (part === "..") {
|
|
604
|
+
if (resolved.length > 1) {
|
|
605
|
+
resolved.pop();
|
|
606
|
+
}
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
resolved.push(part);
|
|
610
|
+
}
|
|
611
|
+
return resolved.join("/") || "/";
|
|
612
|
+
}
|
|
613
|
+
function isWithinRoot(path, root) {
|
|
614
|
+
const normalizedPath = normalizePath(path);
|
|
615
|
+
const normalizedRoot = normalizePath(root);
|
|
616
|
+
if (normalizedPath === normalizedRoot) return true;
|
|
617
|
+
return normalizedPath.startsWith(normalizedRoot + "/");
|
|
618
|
+
}
|
|
619
|
+
function matchPathPattern(path, pattern) {
|
|
620
|
+
const normalizedPath = normalizePath(path);
|
|
621
|
+
const normalizedPattern = normalizePath(pattern);
|
|
622
|
+
if (normalizedPattern === "*") return true;
|
|
623
|
+
if (normalizedPattern === normalizedPath) return true;
|
|
624
|
+
const patternParts = normalizedPattern.split("/");
|
|
625
|
+
const pathParts = normalizedPath.split("/");
|
|
626
|
+
return matchParts(pathParts, 0, patternParts, 0);
|
|
627
|
+
}
|
|
628
|
+
function matchParts(pathParts, pi, patternParts, qi) {
|
|
629
|
+
while (pi < pathParts.length && qi < patternParts.length) {
|
|
630
|
+
const pattern = patternParts[qi];
|
|
631
|
+
if (pattern === "**") {
|
|
632
|
+
if (qi === patternParts.length - 1) return true;
|
|
633
|
+
for (let i = pi; i <= pathParts.length; i++) {
|
|
634
|
+
if (matchParts(pathParts, i, patternParts, qi + 1)) {
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
if (pattern === "*") {
|
|
641
|
+
pi++;
|
|
642
|
+
qi++;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (pattern !== pathParts[pi]) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
pi++;
|
|
649
|
+
qi++;
|
|
650
|
+
}
|
|
651
|
+
while (qi < patternParts.length && patternParts[qi] === "**") {
|
|
652
|
+
qi++;
|
|
653
|
+
}
|
|
654
|
+
return pi === pathParts.length && qi === patternParts.length;
|
|
655
|
+
}
|
|
656
|
+
function isPathAllowed(path, constraints) {
|
|
657
|
+
if (constraints.rootDirectory) {
|
|
658
|
+
if (!isWithinRoot(path, constraints.rootDirectory)) {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (constraints.denied && constraints.denied.length > 0) {
|
|
663
|
+
for (const pattern of constraints.denied) {
|
|
664
|
+
if (matchPathPattern(path, pattern)) {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (constraints.allowed && constraints.allowed.length > 0) {
|
|
670
|
+
let matchesAllowed = false;
|
|
671
|
+
for (const pattern of constraints.allowed) {
|
|
672
|
+
if (matchPathPattern(path, pattern)) {
|
|
673
|
+
matchesAllowed = true;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!matchesAllowed) return false;
|
|
678
|
+
}
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
function extractPathArguments(args) {
|
|
682
|
+
const paths = [];
|
|
683
|
+
for (const value of Object.values(args)) {
|
|
684
|
+
if (typeof value === "string" && (value.includes("/") || value.includes("\\"))) {
|
|
685
|
+
paths.push(value);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return paths;
|
|
689
|
+
}
|
|
690
|
+
function ruleMatchesRequest(rule, request) {
|
|
691
|
+
if (!rule.enabled) return false;
|
|
692
|
+
if (rule.permission !== request.requiredPermission) return false;
|
|
693
|
+
if (!toolPatternMatches(rule.toolPattern, request.toolName)) return false;
|
|
694
|
+
if (!trustLevelMeetsMinimum(request.context.trustLevel, rule.minimumTrustLevel)) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
if (rule.argumentConstraints) {
|
|
698
|
+
if (!argumentConstraintsMatch(rule.argumentConstraints, request.arguments)) {
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (rule.pathConstraints) {
|
|
703
|
+
if (!pathConstraintsMatch(rule.pathConstraints, request.arguments)) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
function toolPatternMatches(pattern, toolName) {
|
|
710
|
+
if (pattern === "*") return true;
|
|
711
|
+
const startsWithStar = pattern.startsWith("*");
|
|
712
|
+
const endsWithStar = pattern.endsWith("*");
|
|
713
|
+
if (startsWithStar && endsWithStar) {
|
|
714
|
+
const infix = pattern.slice(1, -1);
|
|
715
|
+
return infix.length > 0 && toolName.includes(infix);
|
|
716
|
+
}
|
|
717
|
+
if (endsWithStar) {
|
|
718
|
+
const prefix = pattern.slice(0, -1);
|
|
719
|
+
return toolName.startsWith(prefix);
|
|
720
|
+
}
|
|
721
|
+
if (startsWithStar) {
|
|
722
|
+
const suffix = pattern.slice(1);
|
|
723
|
+
return toolName.endsWith(suffix);
|
|
724
|
+
}
|
|
725
|
+
return pattern === toolName;
|
|
726
|
+
}
|
|
727
|
+
var TRUST_LEVEL_ORDER = {
|
|
728
|
+
[TrustLevel.UNTRUSTED]: 0,
|
|
729
|
+
[TrustLevel.VERIFIED]: 1,
|
|
730
|
+
[TrustLevel.TRUSTED]: 2
|
|
731
|
+
};
|
|
732
|
+
function trustLevelMeetsMinimum(actual, minimum) {
|
|
733
|
+
return (TRUST_LEVEL_ORDER[actual] ?? -1) >= (TRUST_LEVEL_ORDER[minimum] ?? Infinity);
|
|
734
|
+
}
|
|
735
|
+
function argumentConstraintsMatch(constraints, args) {
|
|
736
|
+
for (const [key, constraint] of Object.entries(constraints)) {
|
|
737
|
+
if (!(key in args)) return false;
|
|
738
|
+
if (typeof constraint === "string" && typeof args[key] === "string") {
|
|
739
|
+
if (constraint !== "*" && args[key] !== constraint) return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
function pathConstraintsMatch(constraints, args) {
|
|
745
|
+
const paths = extractPathArguments(args);
|
|
746
|
+
if (paths.length === 0) return true;
|
|
747
|
+
return paths.every((path) => isPathAllowed(path, constraints));
|
|
748
|
+
}
|
|
749
|
+
function evaluatePolicy(policySet, request) {
|
|
750
|
+
const startTime = performance.now();
|
|
751
|
+
const sortedRules = [...policySet.rules].sort(
|
|
752
|
+
(a, b) => a.priority - b.priority
|
|
753
|
+
);
|
|
754
|
+
for (const rule of sortedRules) {
|
|
755
|
+
if (ruleMatchesRequest(rule, request)) {
|
|
756
|
+
const endTime2 = performance.now();
|
|
757
|
+
return {
|
|
758
|
+
effect: rule.effect,
|
|
759
|
+
matchedRule: rule,
|
|
760
|
+
reason: `Matched rule "${rule.id}": ${rule.description}`,
|
|
761
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
762
|
+
evaluationTimeMs: endTime2 - startTime
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const endTime = performance.now();
|
|
767
|
+
return {
|
|
768
|
+
effect: DEFAULT_POLICY_EFFECT,
|
|
769
|
+
matchedRule: null,
|
|
770
|
+
reason: "No matching policy rule found. Default action: DENY.",
|
|
771
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
772
|
+
evaluationTimeMs: endTime - startTime
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function validatePolicyRule(input) {
|
|
776
|
+
const errors = [];
|
|
777
|
+
const warnings = [];
|
|
778
|
+
const result = PolicyRuleSchema.safeParse(input);
|
|
779
|
+
if (!result.success) {
|
|
780
|
+
return {
|
|
781
|
+
valid: false,
|
|
782
|
+
errors: result.error.errors.map(
|
|
783
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
784
|
+
),
|
|
785
|
+
warnings: []
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const rule = result.data;
|
|
789
|
+
if (rule.toolPattern === "*" && rule.effect === "ALLOW") {
|
|
790
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.WILDCARD_ALLOW);
|
|
791
|
+
}
|
|
792
|
+
if (rule.minimumTrustLevel === "TRUSTED") {
|
|
793
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.TRUSTED_LEVEL_EXTERNAL);
|
|
794
|
+
}
|
|
795
|
+
if (rule.permission === "EXECUTE") {
|
|
796
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.EXECUTE_WITHOUT_REVIEW);
|
|
797
|
+
}
|
|
798
|
+
return { valid: true, errors, warnings };
|
|
799
|
+
}
|
|
800
|
+
function validatePolicySet(input) {
|
|
801
|
+
const errors = [];
|
|
802
|
+
const warnings = [];
|
|
803
|
+
const result = PolicySetSchema.safeParse(input);
|
|
804
|
+
if (!result.success) {
|
|
805
|
+
return {
|
|
806
|
+
valid: false,
|
|
807
|
+
errors: result.error.errors.map(
|
|
808
|
+
(e) => `${e.path.join(".")}: ${e.message}`
|
|
809
|
+
),
|
|
810
|
+
warnings: []
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const policySet = result.data;
|
|
814
|
+
if (policySet.rules.length > MAX_RULES_PER_POLICY_SET) {
|
|
815
|
+
errors.push(
|
|
816
|
+
`Policy set exceeds maximum of ${MAX_RULES_PER_POLICY_SET} rules`
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const ruleIds = /* @__PURE__ */ new Set();
|
|
820
|
+
for (const rule of policySet.rules) {
|
|
821
|
+
if (ruleIds.has(rule.id)) {
|
|
822
|
+
errors.push(`Duplicate rule ID: "${rule.id}"`);
|
|
823
|
+
}
|
|
824
|
+
ruleIds.add(rule.id);
|
|
825
|
+
}
|
|
826
|
+
for (const rule of policySet.rules) {
|
|
827
|
+
const ruleResult = validatePolicyRule(rule);
|
|
828
|
+
warnings.push(...ruleResult.warnings);
|
|
829
|
+
}
|
|
830
|
+
const hasDenyRule = policySet.rules.some((r) => r.effect === "DENY");
|
|
831
|
+
if (!hasDenyRule && policySet.rules.length > 0) {
|
|
832
|
+
warnings.push(
|
|
833
|
+
"Policy set contains only ALLOW rules. The default-deny fallback is the only protection."
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
valid: errors.length === 0,
|
|
838
|
+
errors,
|
|
839
|
+
warnings
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function analyzeSecurityWarnings(policySet) {
|
|
843
|
+
const warnings = [];
|
|
844
|
+
for (const rule of policySet.rules) {
|
|
845
|
+
warnings.push(...analyzeRuleWarnings(rule));
|
|
846
|
+
}
|
|
847
|
+
const allowRules = policySet.rules.filter(
|
|
848
|
+
(r) => r.effect === "ALLOW" && r.enabled
|
|
849
|
+
);
|
|
850
|
+
const wildcardAllows = allowRules.filter((r) => r.toolPattern === "*");
|
|
851
|
+
if (wildcardAllows.length > 0) {
|
|
852
|
+
warnings.push({
|
|
853
|
+
level: "CRITICAL",
|
|
854
|
+
code: "WILDCARD_ALLOW",
|
|
855
|
+
message: UNSAFE_CONFIGURATION_WARNINGS.WILDCARD_ALLOW,
|
|
856
|
+
recommendation: "Replace wildcard ALLOW rules with specific tool patterns."
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return warnings;
|
|
860
|
+
}
|
|
861
|
+
function analyzeRuleWarnings(rule) {
|
|
862
|
+
const warnings = [];
|
|
863
|
+
if (rule.effect === "ALLOW" && rule.minimumTrustLevel === "UNTRUSTED") {
|
|
864
|
+
warnings.push({
|
|
865
|
+
level: "CRITICAL",
|
|
866
|
+
code: "ALLOW_UNTRUSTED",
|
|
867
|
+
message: `Rule "${rule.id}" allows execution for UNTRUSTED requests. Unverified LLM requests can execute tools.`,
|
|
868
|
+
ruleId: rule.id,
|
|
869
|
+
recommendation: "Set minimumTrustLevel to VERIFIED or higher for ALLOW rules."
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (rule.effect === "ALLOW" && rule.permission === "EXECUTE") {
|
|
873
|
+
warnings.push({
|
|
874
|
+
level: "WARNING",
|
|
875
|
+
code: "ALLOW_EXECUTE",
|
|
876
|
+
message: UNSAFE_CONFIGURATION_WARNINGS.EXECUTE_WITHOUT_REVIEW,
|
|
877
|
+
ruleId: rule.id,
|
|
878
|
+
recommendation: "Ensure EXECUTE permissions are intentional and scoped to specific tools."
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return warnings;
|
|
882
|
+
}
|
|
883
|
+
function createDefaultDenyPolicySet() {
|
|
884
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
885
|
+
return {
|
|
886
|
+
id: "default-deny",
|
|
887
|
+
name: "Default Deny All",
|
|
888
|
+
description: "Denies all tool executions. Add explicit ALLOW rules to grant access to specific tools.",
|
|
889
|
+
version: 1,
|
|
890
|
+
rules: [
|
|
891
|
+
{
|
|
892
|
+
id: "deny-all-execute",
|
|
893
|
+
description: "Explicitly deny all tool executions",
|
|
894
|
+
effect: PolicyEffect.DENY,
|
|
895
|
+
priority: 1e4,
|
|
896
|
+
toolPattern: "*",
|
|
897
|
+
permission: Permission.EXECUTE,
|
|
898
|
+
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
899
|
+
enabled: true,
|
|
900
|
+
createdAt: now,
|
|
901
|
+
updatedAt: now
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
id: "deny-all-write",
|
|
905
|
+
description: "Explicitly deny all write operations",
|
|
906
|
+
effect: PolicyEffect.DENY,
|
|
907
|
+
priority: 1e4,
|
|
908
|
+
toolPattern: "*",
|
|
909
|
+
permission: Permission.WRITE,
|
|
910
|
+
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
911
|
+
enabled: true,
|
|
912
|
+
createdAt: now,
|
|
913
|
+
updatedAt: now
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: "deny-all-read",
|
|
917
|
+
description: "Explicitly deny all read operations",
|
|
918
|
+
effect: PolicyEffect.DENY,
|
|
919
|
+
priority: 1e4,
|
|
920
|
+
toolPattern: "*",
|
|
921
|
+
permission: Permission.READ,
|
|
922
|
+
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
923
|
+
enabled: true,
|
|
924
|
+
createdAt: now,
|
|
925
|
+
updatedAt: now
|
|
926
|
+
}
|
|
927
|
+
],
|
|
928
|
+
createdAt: now,
|
|
929
|
+
updatedAt: now
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
var PolicyEngine = class {
|
|
933
|
+
policySet;
|
|
934
|
+
timeoutMs;
|
|
935
|
+
store;
|
|
936
|
+
constructor(options) {
|
|
937
|
+
this.policySet = options?.policySet ?? createDefaultDenyPolicySet();
|
|
938
|
+
this.timeoutMs = options?.timeoutMs ?? POLICY_EVALUATION_TIMEOUT_MS;
|
|
939
|
+
this.store = options?.store ?? null;
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Evaluates an execution request against the current policy set.
|
|
943
|
+
* Never throws for denials - denial is a normal outcome, not an error.
|
|
944
|
+
*/
|
|
945
|
+
evaluate(request) {
|
|
946
|
+
const startTime = performance.now();
|
|
947
|
+
const decision = evaluatePolicy(this.policySet, request);
|
|
948
|
+
const elapsed = performance.now() - startTime;
|
|
949
|
+
if (elapsed > this.timeoutMs) {
|
|
950
|
+
console.warn(
|
|
951
|
+
`[SolonGate] Policy evaluation took ${elapsed.toFixed(1)}ms (limit: ${this.timeoutMs}ms) for tool "${request.toolName}"`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return decision;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Loads a new policy set, replacing the current one.
|
|
958
|
+
* Validates before accepting. Auto-saves version when store is present.
|
|
959
|
+
*/
|
|
960
|
+
loadPolicySet(policySet, options) {
|
|
961
|
+
const validation = validatePolicySet(policySet);
|
|
962
|
+
if (!validation.valid) {
|
|
963
|
+
return validation;
|
|
964
|
+
}
|
|
965
|
+
this.policySet = policySet;
|
|
966
|
+
if (this.store) {
|
|
967
|
+
this.store.saveVersion(
|
|
968
|
+
policySet,
|
|
969
|
+
options?.reason ?? "Policy updated",
|
|
970
|
+
options?.createdBy ?? "system"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return validation;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Rolls back to a previous policy version.
|
|
977
|
+
* Only available when a PolicyStore is configured.
|
|
978
|
+
*/
|
|
979
|
+
rollback(version) {
|
|
980
|
+
if (!this.store) {
|
|
981
|
+
throw new Error("PolicyStore not configured - cannot rollback");
|
|
982
|
+
}
|
|
983
|
+
const policyVersion = this.store.rollback(this.policySet.id, version);
|
|
984
|
+
this.policySet = policyVersion.policySet;
|
|
985
|
+
return policyVersion;
|
|
986
|
+
}
|
|
987
|
+
getPolicySet() {
|
|
988
|
+
return this.policySet;
|
|
989
|
+
}
|
|
990
|
+
getSecurityWarnings() {
|
|
991
|
+
return analyzeSecurityWarnings(this.policySet);
|
|
992
|
+
}
|
|
993
|
+
getStore() {
|
|
994
|
+
return this.store;
|
|
995
|
+
}
|
|
996
|
+
reset() {
|
|
997
|
+
this.policySet = createDefaultDenyPolicySet();
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
var PolicyStore = class {
|
|
1001
|
+
versions = /* @__PURE__ */ new Map();
|
|
1002
|
+
/**
|
|
1003
|
+
* Saves a new version of a policy set.
|
|
1004
|
+
* The version number auto-increments.
|
|
1005
|
+
*/
|
|
1006
|
+
saveVersion(policySet, reason, createdBy) {
|
|
1007
|
+
const id = policySet.id;
|
|
1008
|
+
const history = this.versions.get(id) ?? [];
|
|
1009
|
+
const latestVersion = history.length > 0 ? history[history.length - 1].version : 0;
|
|
1010
|
+
const version = {
|
|
1011
|
+
version: latestVersion + 1,
|
|
1012
|
+
policySet: Object.freeze({ ...policySet }),
|
|
1013
|
+
hash: this.computeHash(policySet),
|
|
1014
|
+
reason,
|
|
1015
|
+
createdBy,
|
|
1016
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1017
|
+
};
|
|
1018
|
+
const newHistory = [...history, version];
|
|
1019
|
+
this.versions.set(id, newHistory);
|
|
1020
|
+
return version;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Gets a specific version of a policy set.
|
|
1024
|
+
*/
|
|
1025
|
+
getVersion(id, version) {
|
|
1026
|
+
const history = this.versions.get(id);
|
|
1027
|
+
if (!history) return null;
|
|
1028
|
+
return history.find((v) => v.version === version) ?? null;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Gets the latest version of a policy set.
|
|
1032
|
+
*/
|
|
1033
|
+
getLatest(id) {
|
|
1034
|
+
const history = this.versions.get(id);
|
|
1035
|
+
if (!history || history.length === 0) return null;
|
|
1036
|
+
return history[history.length - 1];
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Gets the full version history of a policy set.
|
|
1040
|
+
*/
|
|
1041
|
+
getHistory(id) {
|
|
1042
|
+
return this.versions.get(id) ?? [];
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Rolls back to a previous version by creating a new version
|
|
1046
|
+
* with the same content as the target version.
|
|
1047
|
+
*/
|
|
1048
|
+
rollback(id, toVersion) {
|
|
1049
|
+
const target = this.getVersion(id, toVersion);
|
|
1050
|
+
if (!target) {
|
|
1051
|
+
throw new Error(`Version ${toVersion} not found for policy "${id}"`);
|
|
1052
|
+
}
|
|
1053
|
+
return this.saveVersion(
|
|
1054
|
+
target.policySet,
|
|
1055
|
+
`Rollback to version ${toVersion}`,
|
|
1056
|
+
"system"
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Computes a diff between two policy versions.
|
|
1061
|
+
*/
|
|
1062
|
+
diff(v1, v2) {
|
|
1063
|
+
const oldRulesMap = new Map(v1.policySet.rules.map((r) => [r.id, r]));
|
|
1064
|
+
const newRulesMap = new Map(v2.policySet.rules.map((r) => [r.id, r]));
|
|
1065
|
+
const added = [];
|
|
1066
|
+
const removed = [];
|
|
1067
|
+
const modified = [];
|
|
1068
|
+
for (const [id, newRule] of newRulesMap) {
|
|
1069
|
+
const oldRule = oldRulesMap.get(id);
|
|
1070
|
+
if (!oldRule) {
|
|
1071
|
+
added.push(newRule);
|
|
1072
|
+
} else if (JSON.stringify(oldRule) !== JSON.stringify(newRule)) {
|
|
1073
|
+
modified.push({ old: oldRule, new: newRule });
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (const [id, oldRule] of oldRulesMap) {
|
|
1077
|
+
if (!newRulesMap.has(id)) {
|
|
1078
|
+
removed.push(oldRule);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return { added, removed, modified };
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Computes SHA256 hash of a policy set for integrity verification.
|
|
1085
|
+
*/
|
|
1086
|
+
computeHash(policySet) {
|
|
1087
|
+
const serialized = JSON.stringify(policySet, Object.keys(policySet).sort());
|
|
1088
|
+
return createHash("sha256").update(serialized).digest("hex");
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
// ../sdk-ts/dist/index.js
|
|
1093
|
+
import { randomUUID, createHmac } from "crypto";
|
|
1094
|
+
var DEFAULT_CONFIG = Object.freeze({
|
|
1095
|
+
validateSchemas: true,
|
|
1096
|
+
enableLogging: true,
|
|
1097
|
+
logLevel: "info",
|
|
1098
|
+
evaluationTimeoutMs: 100,
|
|
1099
|
+
verboseErrors: false,
|
|
1100
|
+
globalRateLimitPerMinute: 600,
|
|
1101
|
+
rateLimitPerTool: 60,
|
|
1102
|
+
tokenTtlSeconds: 30,
|
|
1103
|
+
inputGuardConfig: DEFAULT_INPUT_GUARD_CONFIG,
|
|
1104
|
+
enableVersionedPolicies: true
|
|
1105
|
+
});
|
|
1106
|
+
function resolveConfig(userConfig) {
|
|
1107
|
+
const warnings = [];
|
|
1108
|
+
const config = { ...DEFAULT_CONFIG, ...userConfig };
|
|
1109
|
+
if (!config.validateSchemas) {
|
|
1110
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.DISABLED_VALIDATION);
|
|
1111
|
+
}
|
|
1112
|
+
if (config.globalRateLimitPerMinute === 0) {
|
|
1113
|
+
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.RATE_LIMIT_ZERO);
|
|
1114
|
+
}
|
|
1115
|
+
if (config.verboseErrors) {
|
|
1116
|
+
warnings.push(
|
|
1117
|
+
"Verbose errors enabled: internal error details will be sent to the LLM."
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
if (config.tokenSecret && config.tokenSecret.length < 32) {
|
|
1121
|
+
warnings.push(
|
|
1122
|
+
"Token secret is shorter than 32 characters. Use a longer secret for production."
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
return { config, warnings };
|
|
1126
|
+
}
|
|
1127
|
+
async function interceptToolCall(params, upstreamCall, options) {
|
|
1128
|
+
const requestId = randomUUID();
|
|
1129
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1130
|
+
const context = createSecurityContext({ requestId });
|
|
1131
|
+
const request = {
|
|
1132
|
+
context,
|
|
1133
|
+
toolName: params.name,
|
|
1134
|
+
serverName: "default",
|
|
1135
|
+
arguments: params.arguments ?? {},
|
|
1136
|
+
requiredPermission: Permission.EXECUTE,
|
|
1137
|
+
timestamp
|
|
1138
|
+
};
|
|
1139
|
+
if (options.rateLimiter) {
|
|
1140
|
+
if (options.rateLimitPerTool) {
|
|
1141
|
+
const toolLimit = options.rateLimiter.checkLimit(
|
|
1142
|
+
params.name,
|
|
1143
|
+
options.rateLimitPerTool
|
|
1144
|
+
);
|
|
1145
|
+
if (!toolLimit.allowed) {
|
|
1146
|
+
const result = {
|
|
1147
|
+
status: "ERROR",
|
|
1148
|
+
request,
|
|
1149
|
+
error: new RateLimitError(params.name, options.rateLimitPerTool),
|
|
1150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1151
|
+
};
|
|
1152
|
+
options.onDecision?.(result);
|
|
1153
|
+
return createDeniedToolResult(
|
|
1154
|
+
`Rate limit exceeded for tool "${params.name}"`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (options.globalRateLimitPerMinute) {
|
|
1159
|
+
const globalLimit = options.rateLimiter.checkGlobalLimit(
|
|
1160
|
+
options.globalRateLimitPerMinute
|
|
1161
|
+
);
|
|
1162
|
+
if (!globalLimit.allowed) {
|
|
1163
|
+
const result = {
|
|
1164
|
+
status: "ERROR",
|
|
1165
|
+
request,
|
|
1166
|
+
error: new RateLimitError("*", options.globalRateLimitPerMinute),
|
|
1167
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1168
|
+
};
|
|
1169
|
+
options.onDecision?.(result);
|
|
1170
|
+
return createDeniedToolResult("Global rate limit exceeded");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
if (options.validateSchemas && params.arguments) {
|
|
1175
|
+
const guardConfig = options.inputGuardConfig ?? DEFAULT_INPUT_GUARD_CONFIG;
|
|
1176
|
+
const sanitization = sanitizeInput("arguments", params.arguments, guardConfig);
|
|
1177
|
+
if (!sanitization.safe) {
|
|
1178
|
+
const threatDescriptions = sanitization.threats.map(
|
|
1179
|
+
(t) => `${t.type}: ${t.description} (field: ${t.field})`
|
|
1180
|
+
);
|
|
1181
|
+
const result = {
|
|
1182
|
+
status: "ERROR",
|
|
1183
|
+
request,
|
|
1184
|
+
error: new SchemaValidationError(params.name, threatDescriptions),
|
|
1185
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1186
|
+
};
|
|
1187
|
+
options.onDecision?.(result);
|
|
1188
|
+
const reason = options.verboseErrors ? `Input validation failed: ${threatDescriptions.join("; ")}` : "Input validation failed.";
|
|
1189
|
+
return createDeniedToolResult(reason);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const decision = options.policyEngine.evaluate(request);
|
|
1193
|
+
if (decision.effect === "DENY") {
|
|
1194
|
+
const result = {
|
|
1195
|
+
status: "DENIED",
|
|
1196
|
+
request,
|
|
1197
|
+
decision,
|
|
1198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1199
|
+
};
|
|
1200
|
+
options.onDecision?.(result);
|
|
1201
|
+
const reason = options.verboseErrors ? decision.reason : "Tool execution denied by security policy.";
|
|
1202
|
+
return createDeniedToolResult(reason);
|
|
1203
|
+
}
|
|
1204
|
+
let capabilityToken;
|
|
1205
|
+
if (options.tokenIssuer) {
|
|
1206
|
+
capabilityToken = options.tokenIssuer.issue(
|
|
1207
|
+
requestId,
|
|
1208
|
+
[Permission.EXECUTE],
|
|
1209
|
+
[params.name]
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
if (options.serverVerifier && capabilityToken) {
|
|
1213
|
+
options.serverVerifier.createSignedRequest(params, capabilityToken);
|
|
1214
|
+
}
|
|
1215
|
+
try {
|
|
1216
|
+
const startTime = performance.now();
|
|
1217
|
+
const toolResult = await upstreamCall(params);
|
|
1218
|
+
const durationMs = performance.now() - startTime;
|
|
1219
|
+
if (options.rateLimiter) {
|
|
1220
|
+
options.rateLimiter.recordCall(params.name);
|
|
1221
|
+
}
|
|
1222
|
+
const result = {
|
|
1223
|
+
status: "ALLOWED",
|
|
1224
|
+
request,
|
|
1225
|
+
decision,
|
|
1226
|
+
toolResult,
|
|
1227
|
+
durationMs,
|
|
1228
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1229
|
+
};
|
|
1230
|
+
options.onDecision?.(result);
|
|
1231
|
+
return toolResult;
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
const result = {
|
|
1234
|
+
status: "ERROR",
|
|
1235
|
+
request,
|
|
1236
|
+
error: error instanceof Error ? new PolicyDeniedError(params.name, error.message) : new PolicyDeniedError(params.name, "Unknown upstream error"),
|
|
1237
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1238
|
+
};
|
|
1239
|
+
options.onDecision?.(result);
|
|
1240
|
+
throw error;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
var LOG_LEVEL_ORDER = {
|
|
1244
|
+
debug: 0,
|
|
1245
|
+
info: 1,
|
|
1246
|
+
warn: 2,
|
|
1247
|
+
error: 3
|
|
1248
|
+
};
|
|
1249
|
+
var SecurityLogger = class {
|
|
1250
|
+
minLevel;
|
|
1251
|
+
enabled;
|
|
1252
|
+
constructor(options) {
|
|
1253
|
+
this.minLevel = options.level;
|
|
1254
|
+
this.enabled = options.enabled;
|
|
1255
|
+
}
|
|
1256
|
+
logDecision(result) {
|
|
1257
|
+
if (!this.enabled) return;
|
|
1258
|
+
const entry = {
|
|
1259
|
+
type: "security_decision",
|
|
1260
|
+
status: result.status,
|
|
1261
|
+
toolName: result.request.toolName,
|
|
1262
|
+
permission: result.request.requiredPermission,
|
|
1263
|
+
trustLevel: result.request.context.trustLevel,
|
|
1264
|
+
requestId: result.request.context.requestId,
|
|
1265
|
+
timestamp: result.timestamp,
|
|
1266
|
+
...result.status === "ALLOWED" && { durationMs: result.durationMs },
|
|
1267
|
+
...result.status === "DENIED" && { reason: result.decision.reason },
|
|
1268
|
+
...result.status === "ERROR" && { error: result.error.code }
|
|
1269
|
+
};
|
|
1270
|
+
if (result.status === "DENIED" || result.status === "ERROR") {
|
|
1271
|
+
this.log("warn", entry);
|
|
1272
|
+
} else {
|
|
1273
|
+
this.log("info", entry);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
log(level, data) {
|
|
1277
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[this.minLevel]) return;
|
|
1278
|
+
const output = JSON.stringify({ level, ...data });
|
|
1279
|
+
switch (level) {
|
|
1280
|
+
case "error":
|
|
1281
|
+
console.error(`[SolonGate] ${output}`);
|
|
1282
|
+
break;
|
|
1283
|
+
case "warn":
|
|
1284
|
+
console.warn(`[SolonGate] ${output}`);
|
|
1285
|
+
break;
|
|
1286
|
+
case "debug":
|
|
1287
|
+
console.debug(`[SolonGate] ${output}`);
|
|
1288
|
+
break;
|
|
1289
|
+
default:
|
|
1290
|
+
console.info(`[SolonGate] ${output}`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
var TokenIssuer = class {
|
|
1295
|
+
secret;
|
|
1296
|
+
ttlSeconds;
|
|
1297
|
+
issuer;
|
|
1298
|
+
usedNonces = /* @__PURE__ */ new Set();
|
|
1299
|
+
revokedTokens = /* @__PURE__ */ new Set();
|
|
1300
|
+
constructor(config) {
|
|
1301
|
+
if (config.secret.length < MIN_SECRET_LENGTH) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`Token secret must be at least ${MIN_SECRET_LENGTH} characters`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
this.secret = config.secret;
|
|
1307
|
+
this.ttlSeconds = config.ttlSeconds || DEFAULT_TOKEN_TTL_SECONDS;
|
|
1308
|
+
this.issuer = config.issuer;
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Issues a signed capability token.
|
|
1312
|
+
*/
|
|
1313
|
+
issue(requestId, permissions, toolScope, serverScope = ["*"], pathScope) {
|
|
1314
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1315
|
+
const jti = randomUUID();
|
|
1316
|
+
const payload = {
|
|
1317
|
+
jti,
|
|
1318
|
+
iss: this.issuer,
|
|
1319
|
+
sub: requestId,
|
|
1320
|
+
iat: now,
|
|
1321
|
+
exp: now + this.ttlSeconds,
|
|
1322
|
+
permissions: [...permissions],
|
|
1323
|
+
toolScope: [...toolScope],
|
|
1324
|
+
serverScope: [...serverScope],
|
|
1325
|
+
...pathScope && { pathScope: [...pathScope] }
|
|
1326
|
+
};
|
|
1327
|
+
return this.sign(payload);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Verifies a capability token and consumes the nonce (single-use).
|
|
1331
|
+
*/
|
|
1332
|
+
verify(token) {
|
|
1333
|
+
const parsed = this.parseAndVerify(token);
|
|
1334
|
+
if (!parsed.valid || !parsed.payload) {
|
|
1335
|
+
return parsed;
|
|
1336
|
+
}
|
|
1337
|
+
const payload = parsed.payload;
|
|
1338
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1339
|
+
if (payload.exp <= now) {
|
|
1340
|
+
return { valid: false, reason: "Token expired" };
|
|
1341
|
+
}
|
|
1342
|
+
if (this.revokedTokens.has(payload.jti)) {
|
|
1343
|
+
return { valid: false, reason: "Token has been revoked" };
|
|
1344
|
+
}
|
|
1345
|
+
if (this.usedNonces.has(payload.jti)) {
|
|
1346
|
+
return { valid: false, reason: "Token already used (replay detected)" };
|
|
1347
|
+
}
|
|
1348
|
+
this.usedNonces.add(payload.jti);
|
|
1349
|
+
return { valid: true, payload };
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Revokes a token by its ID.
|
|
1353
|
+
*/
|
|
1354
|
+
revoke(jti) {
|
|
1355
|
+
this.revokedTokens.add(jti);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Checks if a token ID has been revoked.
|
|
1359
|
+
*/
|
|
1360
|
+
isRevoked(jti) {
|
|
1361
|
+
return this.revokedTokens.has(jti);
|
|
1362
|
+
}
|
|
1363
|
+
// --- Internal helpers ---
|
|
1364
|
+
sign(payload) {
|
|
1365
|
+
const header = base64UrlEncode(JSON.stringify({ alg: TOKEN_ALGORITHM, typ: "JWT" }));
|
|
1366
|
+
const body = base64UrlEncode(JSON.stringify(payload));
|
|
1367
|
+
const signature = this.computeSignature(`${header}.${body}`);
|
|
1368
|
+
return `${header}.${body}.${signature}`;
|
|
1369
|
+
}
|
|
1370
|
+
parseAndVerify(token) {
|
|
1371
|
+
const parts = token.split(".");
|
|
1372
|
+
if (parts.length !== 3) {
|
|
1373
|
+
return { valid: false, reason: "Invalid token format" };
|
|
1374
|
+
}
|
|
1375
|
+
const [header, body, signature] = parts;
|
|
1376
|
+
const expectedSignature = this.computeSignature(`${header}.${body}`);
|
|
1377
|
+
if (signature !== expectedSignature) {
|
|
1378
|
+
return { valid: false, reason: "Invalid token signature" };
|
|
1379
|
+
}
|
|
1380
|
+
try {
|
|
1381
|
+
const payload = JSON.parse(base64UrlDecode(body));
|
|
1382
|
+
return { valid: true, payload };
|
|
1383
|
+
} catch {
|
|
1384
|
+
return { valid: false, reason: "Invalid token payload" };
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
computeSignature(data) {
|
|
1388
|
+
return base64UrlEncode(
|
|
1389
|
+
createHmac("sha256", this.secret).update(data).digest("base64")
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
function base64UrlEncode(str) {
|
|
1394
|
+
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1395
|
+
}
|
|
1396
|
+
function base64UrlDecode(str) {
|
|
1397
|
+
const padded = str + "=".repeat((4 - str.length % 4) % 4);
|
|
1398
|
+
return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString();
|
|
1399
|
+
}
|
|
1400
|
+
var ServerVerifier = class {
|
|
1401
|
+
gatewaySecret;
|
|
1402
|
+
maxAgeMs;
|
|
1403
|
+
usedNonces = /* @__PURE__ */ new Set();
|
|
1404
|
+
constructor(config) {
|
|
1405
|
+
if (config.gatewaySecret.length < 32) {
|
|
1406
|
+
throw new Error("Gateway secret must be at least 32 characters");
|
|
1407
|
+
}
|
|
1408
|
+
this.gatewaySecret = config.gatewaySecret;
|
|
1409
|
+
this.maxAgeMs = config.maxAgeMs ?? 6e4;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Computes HMAC signature for request data.
|
|
1413
|
+
*/
|
|
1414
|
+
signRequest(params, capabilityToken) {
|
|
1415
|
+
const data = JSON.stringify({ params, capabilityToken });
|
|
1416
|
+
return createHmac("sha256", this.gatewaySecret).update(data).digest("hex");
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Verifies the HMAC signature of request data.
|
|
1420
|
+
*/
|
|
1421
|
+
verifySignature(params, capabilityToken, signature) {
|
|
1422
|
+
const expected = this.signRequest(params, capabilityToken);
|
|
1423
|
+
if (expected.length !== signature.length) return false;
|
|
1424
|
+
let result = 0;
|
|
1425
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1426
|
+
result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
1427
|
+
}
|
|
1428
|
+
return result === 0;
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Creates a complete signed request including timestamp and nonce.
|
|
1432
|
+
*/
|
|
1433
|
+
createSignedRequest(params, capabilityToken) {
|
|
1434
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1435
|
+
const nonce = randomUUID();
|
|
1436
|
+
const signature = this.signRequest(params, capabilityToken);
|
|
1437
|
+
return {
|
|
1438
|
+
params,
|
|
1439
|
+
capabilityToken,
|
|
1440
|
+
signature,
|
|
1441
|
+
timestamp,
|
|
1442
|
+
nonce
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Validates a complete signed request including timestamp, nonce, and signature.
|
|
1447
|
+
*/
|
|
1448
|
+
validateSignedRequest(request) {
|
|
1449
|
+
const requestTime = new Date(request.timestamp).getTime();
|
|
1450
|
+
const now = Date.now();
|
|
1451
|
+
if (isNaN(requestTime)) {
|
|
1452
|
+
return { valid: false, reason: "Invalid timestamp" };
|
|
1453
|
+
}
|
|
1454
|
+
if (now - requestTime > this.maxAgeMs) {
|
|
1455
|
+
return { valid: false, reason: "Request too old" };
|
|
1456
|
+
}
|
|
1457
|
+
if (requestTime > now + 3e4) {
|
|
1458
|
+
return { valid: false, reason: "Request timestamp in the future" };
|
|
1459
|
+
}
|
|
1460
|
+
if (this.usedNonces.has(request.nonce)) {
|
|
1461
|
+
return { valid: false, reason: "Duplicate nonce (replay detected)" };
|
|
1462
|
+
}
|
|
1463
|
+
if (!this.verifySignature(request.params, request.capabilityToken, request.signature)) {
|
|
1464
|
+
return { valid: false, reason: "Invalid signature" };
|
|
1465
|
+
}
|
|
1466
|
+
this.usedNonces.add(request.nonce);
|
|
1467
|
+
return { valid: true };
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
var RateLimiter = class {
|
|
1471
|
+
windowMs;
|
|
1472
|
+
records = /* @__PURE__ */ new Map();
|
|
1473
|
+
globalRecords = [];
|
|
1474
|
+
constructor(options) {
|
|
1475
|
+
this.windowMs = options?.windowMs ?? RATE_LIMIT_WINDOW_MS;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Checks if a tool call is within the rate limit.
|
|
1479
|
+
* Does NOT record the call - use recordCall() after successful execution.
|
|
1480
|
+
*/
|
|
1481
|
+
checkLimit(toolName, limitPerWindow) {
|
|
1482
|
+
const now = Date.now();
|
|
1483
|
+
const windowStart = now - this.windowMs;
|
|
1484
|
+
const records = this.getActiveRecords(toolName, windowStart);
|
|
1485
|
+
const count = records.length;
|
|
1486
|
+
const allowed = count < limitPerWindow;
|
|
1487
|
+
const remaining = Math.max(0, limitPerWindow - count);
|
|
1488
|
+
const resetAt = records.length > 0 ? records[0].timestamp + this.windowMs : now + this.windowMs;
|
|
1489
|
+
return { allowed, remaining, resetAt };
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Checks the global rate limit across all tools.
|
|
1493
|
+
*/
|
|
1494
|
+
checkGlobalLimit(limitPerWindow) {
|
|
1495
|
+
const now = Date.now();
|
|
1496
|
+
const windowStart = now - this.windowMs;
|
|
1497
|
+
this.globalRecords = this.globalRecords.filter(
|
|
1498
|
+
(r) => r.timestamp > windowStart
|
|
1499
|
+
);
|
|
1500
|
+
const count = this.globalRecords.length;
|
|
1501
|
+
const allowed = count < limitPerWindow;
|
|
1502
|
+
const remaining = Math.max(0, limitPerWindow - count);
|
|
1503
|
+
const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
|
|
1504
|
+
return { allowed, remaining, resetAt };
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Records a tool call for rate limiting.
|
|
1508
|
+
* Call this after successful execution.
|
|
1509
|
+
*/
|
|
1510
|
+
recordCall(toolName) {
|
|
1511
|
+
const now = Date.now();
|
|
1512
|
+
const record = { timestamp: now };
|
|
1513
|
+
const records = this.records.get(toolName) ?? [];
|
|
1514
|
+
records.push(record);
|
|
1515
|
+
if (records.length > RATE_LIMIT_MAX_ENTRIES) {
|
|
1516
|
+
const windowStart = now - this.windowMs;
|
|
1517
|
+
const cleaned = records.filter((r) => r.timestamp > windowStart);
|
|
1518
|
+
this.records.set(toolName, cleaned);
|
|
1519
|
+
} else {
|
|
1520
|
+
this.records.set(toolName, records);
|
|
1521
|
+
}
|
|
1522
|
+
this.globalRecords.push(record);
|
|
1523
|
+
if (this.globalRecords.length > RATE_LIMIT_MAX_ENTRIES) {
|
|
1524
|
+
const windowStart = now - this.windowMs;
|
|
1525
|
+
this.globalRecords = this.globalRecords.filter(
|
|
1526
|
+
(r) => r.timestamp > windowStart
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Gets usage stats for a tool.
|
|
1532
|
+
*/
|
|
1533
|
+
getUsage(toolName) {
|
|
1534
|
+
const now = Date.now();
|
|
1535
|
+
const windowStart = now - this.windowMs;
|
|
1536
|
+
const records = this.getActiveRecords(toolName, windowStart);
|
|
1537
|
+
return { count: records.length, windowStart };
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Resets rate tracking for a specific tool.
|
|
1541
|
+
*/
|
|
1542
|
+
resetTool(toolName) {
|
|
1543
|
+
this.records.delete(toolName);
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Resets all rate tracking.
|
|
1547
|
+
*/
|
|
1548
|
+
resetAll() {
|
|
1549
|
+
this.records.clear();
|
|
1550
|
+
this.globalRecords = [];
|
|
1551
|
+
}
|
|
1552
|
+
getActiveRecords(toolName, windowStart) {
|
|
1553
|
+
const records = this.records.get(toolName) ?? [];
|
|
1554
|
+
const active = records.filter((r) => r.timestamp > windowStart);
|
|
1555
|
+
if (active.length !== records.length) {
|
|
1556
|
+
this.records.set(toolName, active);
|
|
1557
|
+
}
|
|
1558
|
+
return active;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
var SolonGate = class {
|
|
1562
|
+
policyEngine;
|
|
1563
|
+
config;
|
|
1564
|
+
logger;
|
|
1565
|
+
configWarnings;
|
|
1566
|
+
tokenIssuer;
|
|
1567
|
+
serverVerifier;
|
|
1568
|
+
rateLimiter;
|
|
1569
|
+
constructor(options) {
|
|
1570
|
+
const { config, warnings } = resolveConfig(options.config);
|
|
1571
|
+
this.config = config;
|
|
1572
|
+
this.configWarnings = warnings;
|
|
1573
|
+
this.logger = new SecurityLogger({
|
|
1574
|
+
level: config.logLevel,
|
|
1575
|
+
enabled: config.enableLogging
|
|
1576
|
+
});
|
|
1577
|
+
for (const warning of warnings) {
|
|
1578
|
+
console.warn(`[SolonGate] WARNING: ${warning}`);
|
|
1579
|
+
}
|
|
1580
|
+
const store = config.enableVersionedPolicies ? new PolicyStore() : void 0;
|
|
1581
|
+
this.policyEngine = new PolicyEngine({
|
|
1582
|
+
policySet: options.policySet ?? config.policySet,
|
|
1583
|
+
timeoutMs: config.evaluationTimeoutMs,
|
|
1584
|
+
store
|
|
1585
|
+
});
|
|
1586
|
+
this.tokenIssuer = config.tokenSecret ? new TokenIssuer({
|
|
1587
|
+
secret: config.tokenSecret,
|
|
1588
|
+
ttlSeconds: config.tokenTtlSeconds,
|
|
1589
|
+
algorithm: TOKEN_ALGORITHM,
|
|
1590
|
+
issuer: config.tokenIssuer ?? options.name
|
|
1591
|
+
}) : null;
|
|
1592
|
+
this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
|
|
1593
|
+
this.rateLimiter = new RateLimiter();
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Intercept and evaluate a tool call against the full security pipeline.
|
|
1597
|
+
* If denied at any stage, returns an error result without calling upstream.
|
|
1598
|
+
* If allowed, calls upstream and returns the result.
|
|
1599
|
+
*/
|
|
1600
|
+
async executeToolCall(params, upstreamCall) {
|
|
1601
|
+
return interceptToolCall(params, upstreamCall, {
|
|
1602
|
+
policyEngine: this.policyEngine,
|
|
1603
|
+
validateSchemas: this.config.validateSchemas,
|
|
1604
|
+
verboseErrors: this.config.verboseErrors,
|
|
1605
|
+
onDecision: (result) => this.logger.logDecision(result),
|
|
1606
|
+
tokenIssuer: this.tokenIssuer ?? void 0,
|
|
1607
|
+
serverVerifier: this.serverVerifier ?? void 0,
|
|
1608
|
+
rateLimiter: this.rateLimiter,
|
|
1609
|
+
inputGuardConfig: this.config.inputGuardConfig,
|
|
1610
|
+
rateLimitPerTool: this.config.rateLimitPerTool,
|
|
1611
|
+
globalRateLimitPerMinute: this.config.globalRateLimitPerMinute
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
/** Load a new policy set at runtime. */
|
|
1615
|
+
loadPolicy(policySet, options) {
|
|
1616
|
+
return this.policyEngine.loadPolicySet(policySet, options);
|
|
1617
|
+
}
|
|
1618
|
+
/** Get current security warnings. */
|
|
1619
|
+
getWarnings() {
|
|
1620
|
+
return [
|
|
1621
|
+
...this.configWarnings,
|
|
1622
|
+
...this.policyEngine.getSecurityWarnings().map((w) => `[${w.level}] ${w.message}`)
|
|
1623
|
+
];
|
|
1624
|
+
}
|
|
1625
|
+
/** Get the policy engine for direct access. */
|
|
1626
|
+
getPolicyEngine() {
|
|
1627
|
+
return this.policyEngine;
|
|
1628
|
+
}
|
|
1629
|
+
/** Get the rate limiter for direct access. */
|
|
1630
|
+
getRateLimiter() {
|
|
1631
|
+
return this.rateLimiter;
|
|
1632
|
+
}
|
|
1633
|
+
/** Get the token issuer (null if not configured). */
|
|
1634
|
+
getTokenIssuer() {
|
|
1635
|
+
return this.tokenIssuer;
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// src/proxy.ts
|
|
1640
|
+
var log = (...args) => process.stderr.write(`[SolonGate] ${args.map(String).join(" ")}
|
|
1641
|
+
`);
|
|
1642
|
+
var Mutex = class {
|
|
1643
|
+
queue = [];
|
|
1644
|
+
locked = false;
|
|
1645
|
+
async acquire() {
|
|
1646
|
+
if (!this.locked) {
|
|
1647
|
+
this.locked = true;
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
return new Promise((resolve2) => {
|
|
1651
|
+
this.queue.push(resolve2);
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
release() {
|
|
1655
|
+
const next = this.queue.shift();
|
|
1656
|
+
if (next) {
|
|
1657
|
+
next();
|
|
1658
|
+
} else {
|
|
1659
|
+
this.locked = false;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
var SolonGateProxy = class {
|
|
1664
|
+
config;
|
|
1665
|
+
gate;
|
|
1666
|
+
client = null;
|
|
1667
|
+
server = null;
|
|
1668
|
+
callMutex = new Mutex();
|
|
1669
|
+
upstreamTools = [];
|
|
1670
|
+
constructor(config) {
|
|
1671
|
+
this.config = config;
|
|
1672
|
+
this.gate = new SolonGate({
|
|
1673
|
+
name: config.name ?? "solongate-proxy",
|
|
1674
|
+
policySet: config.policy,
|
|
1675
|
+
config: {
|
|
1676
|
+
validateSchemas: config.validateInput ?? true,
|
|
1677
|
+
verboseErrors: config.verbose ?? false,
|
|
1678
|
+
rateLimitPerTool: config.rateLimitPerTool,
|
|
1679
|
+
globalRateLimitPerMinute: config.globalRateLimit
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
const warnings = this.gate.getWarnings();
|
|
1683
|
+
for (const w of warnings) {
|
|
1684
|
+
log("WARNING:", w);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Start the proxy: connect to upstream, then serve downstream.
|
|
1689
|
+
*/
|
|
1690
|
+
async start() {
|
|
1691
|
+
log("Starting SolonGate Proxy...");
|
|
1692
|
+
log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
|
|
1693
|
+
log(`Upstream: ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
|
|
1694
|
+
await this.connectUpstream();
|
|
1695
|
+
await this.discoverTools();
|
|
1696
|
+
this.createServer();
|
|
1697
|
+
await this.serve();
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Connect to the upstream MCP server by spawning it as a child process.
|
|
1701
|
+
*/
|
|
1702
|
+
async connectUpstream() {
|
|
1703
|
+
this.client = new Client(
|
|
1704
|
+
{ name: "solongate-proxy-client", version: "0.1.0" },
|
|
1705
|
+
{ capabilities: {} }
|
|
1706
|
+
);
|
|
1707
|
+
const transport = new StdioClientTransport({
|
|
1708
|
+
command: this.config.upstream.command,
|
|
1709
|
+
args: this.config.upstream.args,
|
|
1710
|
+
env: this.config.upstream.env,
|
|
1711
|
+
cwd: this.config.upstream.cwd,
|
|
1712
|
+
stderr: "pipe"
|
|
1713
|
+
});
|
|
1714
|
+
await this.client.connect(transport);
|
|
1715
|
+
log("Connected to upstream server");
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Discover tools from the upstream server.
|
|
1719
|
+
*/
|
|
1720
|
+
async discoverTools() {
|
|
1721
|
+
if (!this.client) throw new Error("Client not connected");
|
|
1722
|
+
const result = await this.client.listTools();
|
|
1723
|
+
this.upstreamTools = result.tools.map((t) => ({
|
|
1724
|
+
name: t.name,
|
|
1725
|
+
description: t.description,
|
|
1726
|
+
inputSchema: t.inputSchema
|
|
1727
|
+
}));
|
|
1728
|
+
log(`Discovered ${this.upstreamTools.length} tools from upstream:`);
|
|
1729
|
+
for (const tool of this.upstreamTools) {
|
|
1730
|
+
log(` - ${tool.name}: ${tool.description ?? "(no description)"}`);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Create the downstream MCP server with proxied handlers.
|
|
1735
|
+
*/
|
|
1736
|
+
createServer() {
|
|
1737
|
+
this.server = new Server(
|
|
1738
|
+
{
|
|
1739
|
+
name: this.config.name ?? "solongate-proxy",
|
|
1740
|
+
version: "0.1.0"
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
capabilities: {
|
|
1744
|
+
tools: {},
|
|
1745
|
+
// Pass through resources and prompts if upstream supports them
|
|
1746
|
+
resources: {},
|
|
1747
|
+
prompts: {}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
);
|
|
1751
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1752
|
+
return { tools: this.upstreamTools };
|
|
1753
|
+
});
|
|
1754
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1755
|
+
const { name, arguments: args } = request.params;
|
|
1756
|
+
log(`Tool call: ${name}`);
|
|
1757
|
+
await this.callMutex.acquire();
|
|
1758
|
+
try {
|
|
1759
|
+
const result = await this.gate.executeToolCall(
|
|
1760
|
+
{ name, arguments: args ?? {} },
|
|
1761
|
+
async (params) => {
|
|
1762
|
+
if (!this.client) throw new Error("Upstream client disconnected");
|
|
1763
|
+
const upstreamResult = await this.client.callTool({
|
|
1764
|
+
name: params.name,
|
|
1765
|
+
arguments: params.arguments
|
|
1766
|
+
});
|
|
1767
|
+
return upstreamResult;
|
|
1768
|
+
}
|
|
1769
|
+
);
|
|
1770
|
+
log(`Result: ${result.isError ? "DENIED/ERROR" : "ALLOWED"}`);
|
|
1771
|
+
return {
|
|
1772
|
+
content: [...result.content],
|
|
1773
|
+
isError: result.isError
|
|
1774
|
+
};
|
|
1775
|
+
} finally {
|
|
1776
|
+
this.callMutex.release();
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1780
|
+
if (!this.client) return { resources: [] };
|
|
1781
|
+
try {
|
|
1782
|
+
return await this.client.listResources();
|
|
1783
|
+
} catch {
|
|
1784
|
+
return { resources: [] };
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1788
|
+
if (!this.client) throw new Error("Upstream client disconnected");
|
|
1789
|
+
return await this.client.readResource({ uri: request.params.uri });
|
|
1790
|
+
});
|
|
1791
|
+
this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
1792
|
+
if (!this.client) return { resourceTemplates: [] };
|
|
1793
|
+
try {
|
|
1794
|
+
return await this.client.listResourceTemplates();
|
|
1795
|
+
} catch {
|
|
1796
|
+
return { resourceTemplates: [] };
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1800
|
+
if (!this.client) return { prompts: [] };
|
|
1801
|
+
try {
|
|
1802
|
+
return await this.client.listPrompts();
|
|
1803
|
+
} catch {
|
|
1804
|
+
return { prompts: [] };
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1808
|
+
if (!this.client) throw new Error("Upstream client disconnected");
|
|
1809
|
+
return await this.client.getPrompt({
|
|
1810
|
+
name: request.params.name,
|
|
1811
|
+
arguments: request.params.arguments
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Start serving on stdio (downstream to Claude).
|
|
1817
|
+
*/
|
|
1818
|
+
async serve() {
|
|
1819
|
+
if (!this.server) throw new Error("Server not created");
|
|
1820
|
+
const transport = new StdioServerTransport();
|
|
1821
|
+
await this.server.connect(transport);
|
|
1822
|
+
log("Proxy is live. All tool calls are now protected by SolonGate.");
|
|
1823
|
+
log("Waiting for requests...");
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
// src/index.ts
|
|
1828
|
+
console.log = (...args) => {
|
|
1829
|
+
process.stderr.write(`[SolonGate] ${args.map(String).join(" ")}
|
|
1830
|
+
`);
|
|
1831
|
+
};
|
|
1832
|
+
console.warn = (...args) => {
|
|
1833
|
+
process.stderr.write(`[SolonGate WARN] ${args.map(String).join(" ")}
|
|
1834
|
+
`);
|
|
1835
|
+
};
|
|
1836
|
+
console.error = (...args) => {
|
|
1837
|
+
process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
|
|
1838
|
+
`);
|
|
1839
|
+
};
|
|
1840
|
+
async function main() {
|
|
1841
|
+
try {
|
|
1842
|
+
const config = parseArgs(process.argv);
|
|
1843
|
+
const proxy = new SolonGateProxy(config);
|
|
1844
|
+
await proxy.start();
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1847
|
+
process.stderr.write(`[SolonGate] Fatal: ${message}
|
|
1848
|
+
`);
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
main();
|