@provos/ironcurtain 0.4.0 → 0.5.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 +219 -248
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +144 -6
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/constitution-user-base.md +2 -0
- package/dist/config/first-start.js +77 -12
- package/dist/config/first-start.js.map +1 -1
- package/dist/config/generated/compiled-policy.json +199 -72
- package/dist/config/generated/tool-annotations.json +1018 -36
- package/dist/config/index.js +6 -0
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +13 -0
- package/dist/config/paths.d.ts +31 -0
- package/dist/config/paths.js +52 -2
- package/dist/config/paths.js.map +1 -1
- package/dist/config/types.d.ts +10 -0
- package/dist/config/user-config.d.ts +21 -3
- package/dist/config/user-config.js +34 -3
- package/dist/config/user-config.js.map +1 -1
- package/dist/docker/adapters/claude-code.d.ts +4 -0
- package/dist/docker/adapters/claude-code.js +96 -37
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +22 -13
- package/dist/docker/code-mode-proxy.d.ts +34 -0
- package/dist/docker/code-mode-proxy.js +137 -0
- package/dist/docker/code-mode-proxy.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +22 -14
- package/dist/docker/docker-agent-session.js +144 -117
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +51 -0
- package/dist/docker/docker-infrastructure.js +261 -0
- package/dist/docker/docker-infrastructure.js.map +1 -0
- package/dist/docker/docker-manager.js +27 -0
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/mitm-proxy.js +4 -0
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.d.ts +81 -0
- package/dist/docker/oauth-credentials.js +178 -0
- package/dist/docker/oauth-credentials.js.map +1 -0
- package/dist/docker/orientation.d.ts +3 -2
- package/dist/docker/orientation.js +4 -4
- package/dist/docker/orientation.js.map +1 -1
- package/dist/docker/provider-config.d.ts +3 -0
- package/dist/docker/provider-config.js +28 -0
- package/dist/docker/provider-config.js.map +1 -1
- package/dist/docker/pty-session.d.ts +27 -0
- package/dist/docker/pty-session.js +420 -0
- package/dist/docker/pty-session.js.map +1 -0
- package/dist/docker/pty-types.d.ts +24 -0
- package/dist/docker/pty-types.js +12 -0
- package/dist/docker/pty-types.js.map +1 -0
- package/dist/docker/types.d.ts +18 -2
- package/dist/escalation/escalation-watcher.d.ts +44 -0
- package/dist/escalation/escalation-watcher.js +92 -0
- package/dist/escalation/escalation-watcher.js.map +1 -0
- package/dist/escalation/listener-command.d.ts +10 -0
- package/dist/escalation/listener-command.js +407 -0
- package/dist/escalation/listener-command.js.map +1 -0
- package/dist/escalation/listener-state.d.ts +49 -0
- package/dist/escalation/listener-state.js +104 -0
- package/dist/escalation/listener-state.js.map +1 -0
- package/dist/escalation/session-registry.d.ts +17 -0
- package/dist/escalation/session-registry.js +98 -0
- package/dist/escalation/session-registry.js.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.js +2 -3
- package/dist/logger.js.map +1 -1
- package/dist/pipeline/annotate.js +35 -15
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/constitution-customizer.d.ts +2 -1
- package/dist/pipeline/constitution-customizer.js +36 -3
- package/dist/pipeline/constitution-customizer.js.map +1 -1
- package/dist/pipeline/github-identity.d.ts +38 -0
- package/dist/pipeline/github-identity.js +64 -0
- package/dist/pipeline/github-identity.js.map +1 -0
- package/dist/pipeline/handwritten-scenarios.js +34 -0
- package/dist/pipeline/handwritten-scenarios.js.map +1 -1
- package/dist/sandbox/index.d.ts +25 -1
- package/dist/sandbox/index.js +90 -12
- package/dist/sandbox/index.js.map +1 -1
- package/dist/servers/fetch-server.d.ts +6 -1
- package/dist/servers/fetch-server.js +26 -14
- package/dist/servers/fetch-server.js.map +1 -1
- package/dist/session/agent-session.js +9 -5
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/base-transport.d.ts +19 -0
- package/dist/session/base-transport.js +36 -0
- package/dist/session/base-transport.js.map +1 -0
- package/dist/session/cli-transport.d.ts +3 -3
- package/dist/session/cli-transport.js +7 -4
- package/dist/session/cli-transport.js.map +1 -1
- package/dist/session/index.js +24 -106
- package/dist/session/index.js.map +1 -1
- package/dist/session/interaction-log.d.ts +13 -0
- package/dist/session/interaction-log.js +28 -0
- package/dist/session/interaction-log.js.map +1 -0
- package/dist/session/preflight.d.ts +3 -0
- package/dist/session/preflight.js +45 -18
- package/dist/session/preflight.js.map +1 -1
- package/dist/session/prompts.d.ts +9 -3
- package/dist/session/prompts.js +28 -10
- package/dist/session/prompts.js.map +1 -1
- package/dist/session/types.d.ts +2 -0
- package/dist/signal/bot-command.js +5 -0
- package/dist/signal/bot-command.js.map +1 -1
- package/dist/signal/format.d.ts +21 -1
- package/dist/signal/format.js +41 -3
- package/dist/signal/format.js.map +1 -1
- package/dist/signal/signal-bot-daemon.d.ts +53 -18
- package/dist/signal/signal-bot-daemon.js +344 -106
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/signal/signal-config.d.ts +4 -0
- package/dist/signal/signal-config.js +2 -0
- package/dist/signal/signal-config.js.map +1 -1
- package/dist/signal/signal-transport.d.ts +13 -7
- package/dist/signal/signal-transport.js +36 -12
- package/dist/signal/signal-transport.js.map +1 -1
- package/dist/trusted-process/audit-log.d.ts +6 -1
- package/dist/trusted-process/audit-log.js +22 -2
- package/dist/trusted-process/audit-log.js.map +1 -1
- package/dist/trusted-process/audit-redactor.d.ts +25 -0
- package/dist/trusted-process/audit-redactor.js +121 -0
- package/dist/trusted-process/audit-redactor.js.map +1 -0
- package/dist/trusted-process/escalation.d.ts +1 -1
- package/dist/trusted-process/escalation.js +6 -1
- package/dist/trusted-process/escalation.js.map +1 -1
- package/dist/trusted-process/index.d.ts +2 -1
- package/dist/trusted-process/index.js +24 -6
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/mcp-client-manager.js +14 -2
- package/dist/trusted-process/mcp-client-manager.js.map +1 -1
- package/dist/trusted-process/mcp-error-utils.d.ts +11 -0
- package/dist/trusted-process/mcp-error-utils.js +50 -0
- package/dist/trusted-process/mcp-error-utils.js.map +1 -0
- package/dist/trusted-process/mcp-proxy-server.d.ts +115 -1
- package/dist/trusted-process/mcp-proxy-server.js +318 -267
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.d.ts +3 -1
- package/dist/trusted-process/policy-engine.js +17 -6
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/sandbox-integration.d.ts +16 -4
- package/dist/trusted-process/sandbox-integration.js +14 -8
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/server-context.d.ts +21 -0
- package/dist/trusted-process/server-context.js +42 -0
- package/dist/trusted-process/server-context.js.map +1 -0
- package/dist/types/argument-roles.d.ts +12 -2
- package/dist/types/argument-roles.js +37 -4
- package/dist/types/argument-roles.js.map +1 -1
- package/docker/Dockerfile.claude-code +8 -3
- package/docker/entrypoint-claude-code.sh +64 -4
- package/package.json +19 -9
- package/src/config/constitution-user-base.md +2 -0
- package/src/config/generated/compiled-policy.json +199 -72
- package/src/config/generated/tool-annotations.json +1018 -36
- package/src/config/mcp-servers.json +13 -0
- package/dist/docker/managed-proxy.d.ts +0 -31
- package/dist/docker/managed-proxy.js +0 -265
- package/dist/docker/managed-proxy.js.map +0 -1
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* ESCALATION_DIR -- (optional) directory for file-based escalation IPC
|
|
20
20
|
* SESSION_LOG_PATH -- (optional) path for capturing child process stderr
|
|
21
21
|
* SANDBOX_POLICY -- (optional) "enforce" | "warn" (default: "warn")
|
|
22
|
+
* AUDIT_REDACTION -- (optional) "true" to redact PII/credentials in audit log entries
|
|
22
23
|
* SERVER_FILTER -- (optional) when set, only connect to this single server name
|
|
23
24
|
* AUTO_APPROVE_ENABLED -- (optional) "true" to enable auto-approval of escalations
|
|
24
25
|
* AUTO_APPROVE_MODEL_ID -- (optional) qualified model ID for auto-approve LLM
|
|
@@ -33,7 +34,9 @@ import { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequ
|
|
|
33
34
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
34
35
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
35
36
|
import { appendFileSync, existsSync, mkdtempSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
37
|
+
import { atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
|
|
36
38
|
import { join, resolve } from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
37
40
|
import { tmpdir } from 'node:os';
|
|
38
41
|
import { v4 as uuidv4 } from 'uuid';
|
|
39
42
|
import { loadGeneratedPolicy, extractServerDomainAllowlists, getPackageGeneratedDir } from '../config/index.js';
|
|
@@ -49,6 +52,8 @@ import { autoApprove, extractArgsForAutoApprove, readUserContext } from './auto-
|
|
|
49
52
|
import { createLanguageModelFromEnv } from '../config/model-provider.js';
|
|
50
53
|
import { wrapLanguageModel } from 'ai';
|
|
51
54
|
import { createLlmLoggingMiddleware } from '../pipeline/llm-logger.js';
|
|
55
|
+
import { extractMcpErrorMessage } from './mcp-error-utils.js';
|
|
56
|
+
import { updateServerContext, formatServerContext } from './server-context.js';
|
|
52
57
|
import { VERSION } from '../version.js';
|
|
53
58
|
/**
|
|
54
59
|
* Adds a root to a client's root list and waits for the server to
|
|
@@ -162,7 +167,7 @@ function readEscalationResponse(responsePath) {
|
|
|
162
167
|
async function waitForEscalationDecision(escalationDir, request) {
|
|
163
168
|
const requestPath = resolve(escalationDir, `request-${request.escalationId}.json`);
|
|
164
169
|
const responsePath = resolve(escalationDir, `response-${request.escalationId}.json`);
|
|
165
|
-
|
|
170
|
+
atomicWriteJsonSync(requestPath, request);
|
|
166
171
|
const deadline = Date.now() + getEscalationTimeoutMs();
|
|
167
172
|
while (Date.now() < deadline) {
|
|
168
173
|
const decision = readEscalationResponse(responsePath);
|
|
@@ -205,7 +210,12 @@ async function createAutoApproveModel() {
|
|
|
205
210
|
return null;
|
|
206
211
|
}
|
|
207
212
|
}
|
|
208
|
-
|
|
213
|
+
// ── Extracted functions ────────────────────────────────────────────────
|
|
214
|
+
/**
|
|
215
|
+
* Reads and validates proxy environment variables.
|
|
216
|
+
* Returns a typed config object or calls process.exit(1) on missing required vars.
|
|
217
|
+
*/
|
|
218
|
+
export function parseProxyEnvConfig() {
|
|
209
219
|
const auditLogPath = process.env.AUDIT_LOG_PATH ?? './audit.jsonl';
|
|
210
220
|
const serversConfigJson = process.env.MCP_SERVERS_CONFIG;
|
|
211
221
|
const generatedDir = process.env.GENERATED_DIR;
|
|
@@ -229,10 +239,10 @@ async function main() {
|
|
|
229
239
|
: {};
|
|
230
240
|
delete process.env.SERVER_CREDENTIALS;
|
|
231
241
|
const sandboxPolicy = (process.env.SANDBOX_POLICY ?? 'warn');
|
|
242
|
+
const auditRedaction = process.env.AUDIT_REDACTION === 'true';
|
|
232
243
|
const allServersConfig = JSON.parse(serversConfigJson);
|
|
233
244
|
const protectedPaths = JSON.parse(protectedPathsJson);
|
|
234
245
|
// When SERVER_FILTER is set, only connect to that single backend server.
|
|
235
|
-
// This allows per-server proxy processes with clean tool naming.
|
|
236
246
|
const serverFilter = process.env.SERVER_FILTER;
|
|
237
247
|
const serversConfig = serverFilter
|
|
238
248
|
? { [serverFilter]: allServersConfig[serverFilter] }
|
|
@@ -242,17 +252,26 @@ async function main() {
|
|
|
242
252
|
process.stderr.write(`SERVER_FILTER: unknown server "${serverFilter}"\n`);
|
|
243
253
|
process.exit(1);
|
|
244
254
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
return {
|
|
256
|
+
auditLogPath,
|
|
257
|
+
serversConfig,
|
|
258
|
+
generatedDir,
|
|
259
|
+
protectedPaths,
|
|
260
|
+
sessionLogPath,
|
|
261
|
+
allowedDirectory,
|
|
262
|
+
containerWorkspaceDir,
|
|
263
|
+
escalationDir,
|
|
264
|
+
serverCredentials,
|
|
265
|
+
sandboxPolicy,
|
|
266
|
+
auditRedaction,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Validates sandbox availability against the configured policy.
|
|
271
|
+
* Logs warnings to session log. Throws when enforce mode is active
|
|
272
|
+
* but sandboxing is unavailable.
|
|
273
|
+
*/
|
|
274
|
+
export function validateSandboxAvailability(sandboxPolicy, sessionLogPath, platform) {
|
|
256
275
|
const { platformSupported, errors: depErrors, warnings: depWarnings } = checkSandboxAvailability();
|
|
257
276
|
if (sessionLogPath) {
|
|
258
277
|
for (const warning of depWarnings) {
|
|
@@ -260,7 +279,7 @@ async function main() {
|
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
281
|
if (sandboxPolicy === 'enforce' && (!platformSupported || depErrors.length > 0)) {
|
|
263
|
-
const reasons = !platformSupported ? [`Platform ${
|
|
282
|
+
const reasons = !platformSupported ? [`Platform ${platform} not supported`] : depErrors;
|
|
264
283
|
throw new Error(`[sandbox] FATAL: sandboxPolicy is "enforce" but sandboxing is unavailable:\n` +
|
|
265
284
|
reasons.map((r) => ` - ${r}`).join('\n') +
|
|
266
285
|
'\n' +
|
|
@@ -268,23 +287,265 @@ async function main() {
|
|
|
268
287
|
}
|
|
269
288
|
const sandboxAvailable = platformSupported && depErrors.length === 0;
|
|
270
289
|
if (!sandboxAvailable && sessionLogPath) {
|
|
271
|
-
const missing = depErrors.length > 0 ? depErrors.join(', ') : `platform ${
|
|
290
|
+
const missing = depErrors.length > 0 ? depErrors.join(', ') : `platform ${platform}`;
|
|
272
291
|
logToSessionFile(sessionLogPath, `[sandbox] WARNING: OS-level sandboxing unavailable (${missing}). ` +
|
|
273
292
|
`Servers will run without OS containment. ` +
|
|
274
293
|
`Set SANDBOX_POLICY=enforce to require sandboxing.`);
|
|
275
294
|
}
|
|
276
|
-
|
|
295
|
+
return { sandboxAvailable };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resolves per-server sandbox configurations and writes srt settings
|
|
299
|
+
* files for sandboxed servers. Returns the config map and the temp
|
|
300
|
+
* settings directory path.
|
|
301
|
+
*/
|
|
302
|
+
export function resolveServerSandboxConfigs(serversConfig, allowedDirectory, sandboxAvailable, sandboxPolicy) {
|
|
277
303
|
const resolvedSandboxConfigs = new Map();
|
|
304
|
+
const serverCwdPaths = new Map();
|
|
278
305
|
const settingsDir = mkdtempSync(join(tmpdir(), 'ironcurtain-srt-'));
|
|
279
306
|
for (const [serverName, config] of Object.entries(serversConfig)) {
|
|
280
307
|
const resolved = resolveSandboxConfig(config, allowedDirectory ?? '/tmp', sandboxAvailable, sandboxPolicy);
|
|
281
308
|
resolvedSandboxConfigs.set(serverName, resolved);
|
|
282
309
|
if (resolved.sandboxed) {
|
|
283
|
-
writeServerSettings(serverName, resolved.config, settingsDir);
|
|
310
|
+
const { cwdPath } = writeServerSettings(serverName, resolved.config, settingsDir);
|
|
311
|
+
serverCwdPaths.set(serverName, cwdPath);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return { resolvedSandboxConfigs, settingsDir, serverCwdPaths };
|
|
315
|
+
}
|
|
316
|
+
/** Builds a lookup map from tool name to ProxiedTool for routing. */
|
|
317
|
+
export function buildToolMap(allTools) {
|
|
318
|
+
const toolMap = new Map();
|
|
319
|
+
for (const tool of allTools) {
|
|
320
|
+
toolMap.set(tool.name, tool);
|
|
321
|
+
}
|
|
322
|
+
return toolMap;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Creates an audit entry and logs it. Extracted so the audit shape
|
|
326
|
+
* construction is testable independently.
|
|
327
|
+
*/
|
|
328
|
+
export function buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, options) {
|
|
329
|
+
return {
|
|
330
|
+
timestamp: request.timestamp,
|
|
331
|
+
requestId: request.requestId,
|
|
332
|
+
serverName: request.serverName,
|
|
333
|
+
toolName: request.toolName,
|
|
334
|
+
arguments: argsForTransport,
|
|
335
|
+
policyDecision,
|
|
336
|
+
escalationResult: options.escalationResult,
|
|
337
|
+
result,
|
|
338
|
+
durationMs,
|
|
339
|
+
sandboxed: options.sandboxed || undefined,
|
|
340
|
+
autoApproved: options.autoApproved || undefined,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Handles a single tool call request: policy evaluation, escalation,
|
|
345
|
+
* circuit breaker check, and forwarding to the real MCP server.
|
|
346
|
+
*
|
|
347
|
+
* This is the core security logic extracted from the CallTool handler
|
|
348
|
+
* for independent unit testing.
|
|
349
|
+
*/
|
|
350
|
+
export async function handleCallTool(toolName, rawArgs, deps) {
|
|
351
|
+
const toolInfo = deps.toolMap.get(toolName);
|
|
352
|
+
if (!toolInfo) {
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
|
|
355
|
+
isError: true,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
// Annotation-driven normalization: split into transport vs policy args
|
|
359
|
+
const annotation = deps.policyEngine.getAnnotation(toolInfo.serverName, toolInfo.name);
|
|
360
|
+
if (!annotation) {
|
|
361
|
+
return {
|
|
362
|
+
content: [
|
|
363
|
+
{
|
|
364
|
+
type: 'text',
|
|
365
|
+
text: `Missing annotation for tool: ${toolInfo.serverName}__${toolInfo.name}. Re-run 'ironcurtain annotate-tools' to update.`,
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
isError: true,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const { argsForTransport, argsForPolicy } = prepareToolArgs(rawArgs, annotation, deps.allowedDirectory, deps.containerWorkspaceDir);
|
|
372
|
+
const request = {
|
|
373
|
+
requestId: uuidv4(),
|
|
374
|
+
serverName: toolInfo.serverName,
|
|
375
|
+
toolName: toolInfo.name,
|
|
376
|
+
arguments: argsForPolicy,
|
|
377
|
+
timestamp: new Date().toISOString(),
|
|
378
|
+
};
|
|
379
|
+
const evaluation = deps.policyEngine.evaluate(request);
|
|
380
|
+
const policyDecision = {
|
|
381
|
+
status: evaluation.decision,
|
|
382
|
+
rule: evaluation.rule,
|
|
383
|
+
reason: evaluation.reason,
|
|
384
|
+
};
|
|
385
|
+
let escalationResult;
|
|
386
|
+
let autoApproved = false;
|
|
387
|
+
const serverSandboxConfig = deps.resolvedSandboxConfigs.get(toolInfo.serverName);
|
|
388
|
+
const serverIsSandboxed = serverSandboxConfig?.sandboxed === true;
|
|
389
|
+
function logAudit(result, durationMs, overrideEscalation) {
|
|
390
|
+
const entry = buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, {
|
|
391
|
+
escalationResult: overrideEscalation ?? escalationResult,
|
|
392
|
+
sandboxed: serverIsSandboxed,
|
|
393
|
+
autoApproved,
|
|
394
|
+
});
|
|
395
|
+
deps.auditLog.log(entry);
|
|
396
|
+
}
|
|
397
|
+
if (evaluation.decision === 'escalate') {
|
|
398
|
+
if (!deps.escalationDir) {
|
|
399
|
+
logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: 'text',
|
|
404
|
+
text: `ESCALATION REQUIRED: ${evaluation.reason}. Action denied (no escalation handler).`,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
isError: true,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// Try auto-approve before falling through to human escalation
|
|
411
|
+
if (deps.autoApproveModel) {
|
|
412
|
+
const userMessage = readUserContext(deps.escalationDir);
|
|
413
|
+
if (userMessage) {
|
|
414
|
+
const autoResult = await autoApprove({
|
|
415
|
+
userMessage,
|
|
416
|
+
toolName: `${toolInfo.serverName}/${toolInfo.name}`,
|
|
417
|
+
escalationReason: evaluation.reason,
|
|
418
|
+
arguments: extractArgsForAutoApprove(argsForPolicy, annotation),
|
|
419
|
+
}, deps.autoApproveModel);
|
|
420
|
+
if (autoResult.decision === 'approve') {
|
|
421
|
+
autoApproved = true;
|
|
422
|
+
escalationResult = 'approved';
|
|
423
|
+
policyDecision.status = 'allow';
|
|
424
|
+
policyDecision.reason = `Auto-approved: ${autoResult.reasoning}`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (!autoApproved) {
|
|
429
|
+
const escalationId = uuidv4();
|
|
430
|
+
const decision = await waitForEscalationDecision(deps.escalationDir, {
|
|
431
|
+
escalationId,
|
|
432
|
+
serverName: request.serverName,
|
|
433
|
+
toolName: request.toolName,
|
|
434
|
+
arguments: argsForTransport,
|
|
435
|
+
reason: evaluation.reason,
|
|
436
|
+
context: formatServerContext(deps.serverContextMap, toolInfo.serverName),
|
|
437
|
+
});
|
|
438
|
+
if (decision === 'denied') {
|
|
439
|
+
logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
|
|
440
|
+
return {
|
|
441
|
+
content: [{ type: 'text', text: `ESCALATION DENIED: ${evaluation.reason}` }],
|
|
442
|
+
isError: true,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
escalationResult = 'approved';
|
|
446
|
+
policyDecision.status = 'allow';
|
|
447
|
+
policyDecision.reason = 'Approved by human during escalation';
|
|
448
|
+
}
|
|
449
|
+
// Expand roots for approved path arguments only (skip URLs, opaques)
|
|
450
|
+
const state = deps.clientStates.get(toolInfo.serverName);
|
|
451
|
+
if (state) {
|
|
452
|
+
const pathValues = extractAnnotatedPaths(argsForTransport, annotation, getPathRoles());
|
|
453
|
+
for (const p of pathValues) {
|
|
454
|
+
await addRootToClient(state, {
|
|
455
|
+
uri: `file://${directoryForPath(p)}`,
|
|
456
|
+
name: 'escalation-approved',
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (evaluation.decision === 'deny') {
|
|
462
|
+
logAudit({ status: 'denied', error: evaluation.reason }, 0);
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: 'text', text: `DENIED: ${evaluation.reason}` }],
|
|
465
|
+
isError: true,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
// Circuit breaker: deny if the same tool+args is called too many times
|
|
469
|
+
const cbVerdict = deps.circuitBreaker.check(toolInfo.name, argsForTransport);
|
|
470
|
+
if (!cbVerdict.allowed) {
|
|
471
|
+
logAudit({ status: 'denied', error: cbVerdict.reason }, 0);
|
|
472
|
+
return {
|
|
473
|
+
content: [{ type: 'text', text: cbVerdict.reason }],
|
|
474
|
+
isError: true,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
// Policy allows -- forward to the real MCP server with transport args
|
|
478
|
+
const startTime = Date.now();
|
|
479
|
+
try {
|
|
480
|
+
const clientState = deps.clientStates.get(toolInfo.serverName);
|
|
481
|
+
if (!clientState) {
|
|
482
|
+
const err = `Internal error: no client connection for server "${toolInfo.serverName}"`;
|
|
483
|
+
logAudit({ status: 'denied', error: err }, 0);
|
|
484
|
+
return { content: [{ type: 'text', text: err }], isError: true };
|
|
485
|
+
}
|
|
486
|
+
const client = clientState.client;
|
|
487
|
+
// TODO(workaround): Remove once @cyanheads/git-mcp-server fixes outputSchema declarations.
|
|
488
|
+
// Uses CompatibilityCallToolResultSchema for permissive response parsing.
|
|
489
|
+
const result = await client.callTool({
|
|
490
|
+
name: toolInfo.name,
|
|
491
|
+
arguments: argsForTransport,
|
|
492
|
+
}, CompatibilityCallToolResultSchema, { timeout: getEscalationTimeoutMs() });
|
|
493
|
+
if (result.isError) {
|
|
494
|
+
const errorText = extractTextFromContent(result.content) ?? 'Unknown tool error';
|
|
495
|
+
const errorMessage = annotateSandboxViolation(errorText, serverIsSandboxed);
|
|
496
|
+
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
497
|
+
return { content: result.content, isError: true };
|
|
498
|
+
}
|
|
499
|
+
updateServerContext(deps.serverContextMap, toolInfo.serverName, toolInfo.name, argsForTransport);
|
|
500
|
+
logAudit({ status: 'success' }, Date.now() - startTime);
|
|
501
|
+
return { content: result.content };
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
const rawError = extractMcpErrorMessage(err);
|
|
505
|
+
const errorMessage = annotateSandboxViolation(rawError, serverIsSandboxed);
|
|
506
|
+
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Selects and validates the proxy transport based on environment variables.
|
|
515
|
+
* Returns 'tcp', 'uds', or 'stdio' along with the transport options.
|
|
516
|
+
*/
|
|
517
|
+
export function selectTransportConfig() {
|
|
518
|
+
const proxyTcpPort = process.env.PROXY_TCP_PORT;
|
|
519
|
+
const proxySocketPath = process.env.PROXY_SOCKET_PATH;
|
|
520
|
+
if (proxyTcpPort) {
|
|
521
|
+
const parsedPort = parseInt(proxyTcpPort, 10);
|
|
522
|
+
if (!Number.isFinite(parsedPort) || parsedPort < 0 || parsedPort > 65535) {
|
|
523
|
+
throw new Error(`Invalid PROXY_TCP_PORT value "${proxyTcpPort}". Expected an integer between 0 and 65535.`);
|
|
284
524
|
}
|
|
525
|
+
return { kind: 'tcp', port: parsedPort, portFilePath: process.env.PROXY_PORT_FILE };
|
|
526
|
+
}
|
|
527
|
+
if (proxySocketPath) {
|
|
528
|
+
return { kind: 'uds', socketPath: proxySocketPath };
|
|
285
529
|
}
|
|
530
|
+
return { kind: 'stdio' };
|
|
531
|
+
}
|
|
532
|
+
// ── main() -- thin orchestrator ────────────────────────────────────────
|
|
533
|
+
async function main() {
|
|
534
|
+
const envConfig = parseProxyEnvConfig();
|
|
535
|
+
const { auditLogPath, serversConfig, generatedDir, protectedPaths, sessionLogPath, allowedDirectory, containerWorkspaceDir, escalationDir, serverCredentials, sandboxPolicy, auditRedaction, } = envConfig;
|
|
536
|
+
const { compiledPolicy, toolAnnotations, dynamicLists } = loadGeneratedPolicy(generatedDir, getPackageGeneratedDir());
|
|
537
|
+
const serverDomainAllowlists = extractServerDomainAllowlists(serversConfig);
|
|
538
|
+
const policyEngine = new PolicyEngine(compiledPolicy, toolAnnotations, protectedPaths, allowedDirectory, serverDomainAllowlists, dynamicLists);
|
|
539
|
+
const auditLog = new AuditLog(auditLogPath, { redact: auditRedaction });
|
|
540
|
+
const circuitBreaker = new CallCircuitBreaker();
|
|
541
|
+
const autoApproveModel = await createAutoApproveModel();
|
|
542
|
+
const policyRoots = extractPolicyRoots(compiledPolicy, allowedDirectory ?? '/tmp');
|
|
543
|
+
const mcpRoots = toMcpRoots(policyRoots);
|
|
544
|
+
// ── Sandbox availability & config resolution ──────────────────────
|
|
545
|
+
const { sandboxAvailable } = validateSandboxAvailability(sandboxPolicy, sessionLogPath, process.platform);
|
|
546
|
+
const { resolvedSandboxConfigs, settingsDir, serverCwdPaths } = resolveServerSandboxConfigs(serversConfig, allowedDirectory, sandboxAvailable, sandboxPolicy);
|
|
547
|
+
// ── Connect to real MCP servers ───────────────────────────────────
|
|
286
548
|
const clientStates = new Map();
|
|
287
|
-
// Connect to real MCP servers as clients, wrapping sandboxed ones with srt
|
|
288
549
|
const allTools = [];
|
|
289
550
|
for (const [serverName, config] of Object.entries(serversConfig)) {
|
|
290
551
|
const resolved = resolvedSandboxConfigs.get(serverName);
|
|
@@ -294,22 +555,13 @@ async function main() {
|
|
|
294
555
|
const transport = new StdioClientTransport({
|
|
295
556
|
command: wrapped.command,
|
|
296
557
|
args: wrapped.args,
|
|
297
|
-
// Always pass full process.env -- never rely on getDefaultEnvironment()
|
|
298
|
-
// which strips vars that srt and MCP servers may need.
|
|
299
|
-
// Server credentials are merged last so they override both system env
|
|
300
|
-
// and static mcp-servers.json env values.
|
|
301
558
|
env: { ...process.env, ...(config.env ?? {}), ...serverCredentials },
|
|
302
|
-
stderr: 'pipe',
|
|
303
|
-
// Sandboxed servers
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
...(resolved.sandboxed && allowedDirectory ? { cwd: allowedDirectory } : {}),
|
|
559
|
+
stderr: 'pipe',
|
|
560
|
+
// Sandboxed servers use a per-server temp dir as CWD (not the sandbox)
|
|
561
|
+
// to prevent srt/bwrap ghost dotfiles from polluting the sandbox directory.
|
|
562
|
+
...(resolved.sandboxed && serverCwdPaths.has(serverName) ? { cwd: serverCwdPaths.get(serverName) } : {}),
|
|
307
563
|
});
|
|
308
|
-
// Capture stderr per-server for diagnostic output on connection failure
|
|
309
564
|
let serverStderr = '';
|
|
310
|
-
// Drain the piped stderr to prevent buffer backpressure from blocking
|
|
311
|
-
// the child process. Write output to the session log if configured.
|
|
312
|
-
// Known credential values are redacted before writing to the log.
|
|
313
565
|
if (transport.stderr) {
|
|
314
566
|
transport.stderr.on('data', (chunk) => {
|
|
315
567
|
const text = chunk.toString();
|
|
@@ -324,12 +576,7 @@ async function main() {
|
|
|
324
576
|
});
|
|
325
577
|
}
|
|
326
578
|
const client = new Client({ name: 'ironcurtain-proxy', version: VERSION }, { capabilities: { roots: { listChanged: true } } });
|
|
327
|
-
// Mutable copy per client -- root expansion pushes to this array
|
|
328
579
|
const state = { client, roots: [...mcpRoots] };
|
|
329
|
-
// When the server asks for roots, return the current set.
|
|
330
|
-
// If a rootsRefreshed callback is registered (from escalation-triggered
|
|
331
|
-
// root expansion), resolve it so the caller knows the server has
|
|
332
|
-
// the latest roots.
|
|
333
580
|
client.setRequestHandler(ListRootsRequestSchema, () => {
|
|
334
581
|
if (state.rootsRefreshed) {
|
|
335
582
|
state.rootsRefreshed();
|
|
@@ -356,16 +603,10 @@ async function main() {
|
|
|
356
603
|
});
|
|
357
604
|
}
|
|
358
605
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
for (const tool of allTools) {
|
|
362
|
-
toolMap.set(tool.name, tool);
|
|
363
|
-
}
|
|
364
|
-
// Create the proxy MCP server using the low-level Server API
|
|
365
|
-
// so we can pass through raw JSON schemas without Zod conversion
|
|
606
|
+
const toolMap = buildToolMap(allTools);
|
|
607
|
+
// ── Create the proxy MCP server ───────────────────────────────────
|
|
366
608
|
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional use of low-level Server for raw JSON schema passthrough
|
|
367
609
|
const server = new Server({ name: 'ironcurtain-proxy', version: VERSION }, { capabilities: { tools: {} } });
|
|
368
|
-
// Handle tools/list -- return all proxied tool schemas verbatim
|
|
369
610
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
370
611
|
return {
|
|
371
612
|
tools: allTools.map((t) => ({
|
|
@@ -375,238 +616,45 @@ async function main() {
|
|
|
375
616
|
})),
|
|
376
617
|
};
|
|
377
618
|
});
|
|
378
|
-
|
|
619
|
+
const serverContextMap = new Map();
|
|
620
|
+
const callToolDeps = {
|
|
621
|
+
toolMap,
|
|
622
|
+
policyEngine,
|
|
623
|
+
auditLog,
|
|
624
|
+
circuitBreaker,
|
|
625
|
+
clientStates,
|
|
626
|
+
resolvedSandboxConfigs,
|
|
627
|
+
allowedDirectory,
|
|
628
|
+
containerWorkspaceDir,
|
|
629
|
+
escalationDir,
|
|
630
|
+
autoApproveModel,
|
|
631
|
+
serverContextMap,
|
|
632
|
+
};
|
|
379
633
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
380
|
-
|
|
381
|
-
const rawArgs = req.params.arguments ?? {};
|
|
382
|
-
const toolInfo = toolMap.get(toolName);
|
|
383
|
-
if (!toolInfo) {
|
|
384
|
-
return {
|
|
385
|
-
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
|
|
386
|
-
isError: true,
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
// Annotation-driven normalization: split into transport vs policy args
|
|
390
|
-
const annotation = policyEngine.getAnnotation(toolInfo.serverName, toolInfo.name);
|
|
391
|
-
if (!annotation) {
|
|
392
|
-
return {
|
|
393
|
-
content: [
|
|
394
|
-
{
|
|
395
|
-
type: 'text',
|
|
396
|
-
text: `Missing annotation for tool: ${toolInfo.serverName}__${toolInfo.name}. Re-run 'ironcurtain annotate-tools' to update.`,
|
|
397
|
-
},
|
|
398
|
-
],
|
|
399
|
-
isError: true,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
const { argsForTransport, argsForPolicy } = prepareToolArgs(rawArgs, annotation, allowedDirectory, containerWorkspaceDir);
|
|
403
|
-
const request = {
|
|
404
|
-
requestId: uuidv4(),
|
|
405
|
-
serverName: toolInfo.serverName,
|
|
406
|
-
toolName: toolInfo.name,
|
|
407
|
-
arguments: argsForPolicy,
|
|
408
|
-
timestamp: new Date().toISOString(),
|
|
409
|
-
};
|
|
410
|
-
const evaluation = policyEngine.evaluate(request);
|
|
411
|
-
const policyDecision = {
|
|
412
|
-
status: evaluation.decision,
|
|
413
|
-
rule: evaluation.rule,
|
|
414
|
-
reason: evaluation.reason,
|
|
415
|
-
};
|
|
416
|
-
// Tracks the escalation outcome for audit logging when an approved
|
|
417
|
-
// escalation falls through to the forwarding section below.
|
|
418
|
-
let escalationResult;
|
|
419
|
-
// Track whether auto-approver handled this escalation
|
|
420
|
-
let autoApproved = false;
|
|
421
|
-
// Look up whether this server is sandboxed for audit logging
|
|
422
|
-
const serverSandboxConfig = resolvedSandboxConfigs.get(toolInfo.serverName);
|
|
423
|
-
const serverIsSandboxed = serverSandboxConfig?.sandboxed === true;
|
|
424
|
-
// Audit log records argsForTransport (what was actually sent to the MCP server)
|
|
425
|
-
function logAudit(result, durationMs, overrideEscalation) {
|
|
426
|
-
const entry = {
|
|
427
|
-
timestamp: request.timestamp,
|
|
428
|
-
requestId: request.requestId,
|
|
429
|
-
serverName: request.serverName,
|
|
430
|
-
toolName: request.toolName,
|
|
431
|
-
arguments: argsForTransport,
|
|
432
|
-
policyDecision,
|
|
433
|
-
escalationResult: overrideEscalation ?? escalationResult,
|
|
434
|
-
result,
|
|
435
|
-
durationMs,
|
|
436
|
-
sandboxed: serverIsSandboxed || undefined,
|
|
437
|
-
autoApproved: autoApproved || undefined,
|
|
438
|
-
};
|
|
439
|
-
auditLog.log(entry);
|
|
440
|
-
}
|
|
441
|
-
if (evaluation.decision === 'escalate') {
|
|
442
|
-
if (!escalationDir) {
|
|
443
|
-
// No escalation directory configured -- auto-deny (backward compatible)
|
|
444
|
-
logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
|
|
445
|
-
return {
|
|
446
|
-
content: [
|
|
447
|
-
{
|
|
448
|
-
type: 'text',
|
|
449
|
-
text: `ESCALATION REQUIRED: ${evaluation.reason}. Action denied (no escalation handler).`,
|
|
450
|
-
},
|
|
451
|
-
],
|
|
452
|
-
isError: true,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
// Try auto-approve before falling through to human escalation
|
|
456
|
-
if (autoApproveModel) {
|
|
457
|
-
const userMessage = readUserContext(escalationDir);
|
|
458
|
-
if (userMessage) {
|
|
459
|
-
const autoResult = await autoApprove({
|
|
460
|
-
userMessage,
|
|
461
|
-
toolName: `${toolInfo.serverName}/${toolInfo.name}`,
|
|
462
|
-
escalationReason: evaluation.reason,
|
|
463
|
-
arguments: extractArgsForAutoApprove(argsForPolicy, annotation),
|
|
464
|
-
}, autoApproveModel);
|
|
465
|
-
if (autoResult.decision === 'approve') {
|
|
466
|
-
autoApproved = true;
|
|
467
|
-
escalationResult = 'approved';
|
|
468
|
-
policyDecision.status = 'allow';
|
|
469
|
-
policyDecision.reason = `Auto-approved: ${autoResult.reasoning}`;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
if (!autoApproved) {
|
|
474
|
-
// File-based escalation rendezvous: write request, poll for response
|
|
475
|
-
const escalationId = uuidv4();
|
|
476
|
-
const decision = await waitForEscalationDecision(escalationDir, {
|
|
477
|
-
escalationId,
|
|
478
|
-
serverName: request.serverName,
|
|
479
|
-
toolName: request.toolName,
|
|
480
|
-
arguments: argsForTransport,
|
|
481
|
-
reason: evaluation.reason,
|
|
482
|
-
});
|
|
483
|
-
if (decision === 'denied') {
|
|
484
|
-
logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
|
|
485
|
-
return {
|
|
486
|
-
content: [{ type: 'text', text: `ESCALATION DENIED: ${evaluation.reason}` }],
|
|
487
|
-
isError: true,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
// Approved by human -- update policy decision and fall through
|
|
491
|
-
escalationResult = 'approved';
|
|
492
|
-
policyDecision.status = 'allow';
|
|
493
|
-
policyDecision.reason = 'Approved by human during escalation';
|
|
494
|
-
}
|
|
495
|
-
// Expand roots for approved path arguments only (skip URLs, opaques)
|
|
496
|
-
const state = clientStates.get(toolInfo.serverName);
|
|
497
|
-
if (state) {
|
|
498
|
-
const pathValues = extractAnnotatedPaths(argsForTransport, annotation, getPathRoles());
|
|
499
|
-
for (const p of pathValues) {
|
|
500
|
-
await addRootToClient(state, {
|
|
501
|
-
uri: `file://${directoryForPath(p)}`,
|
|
502
|
-
name: 'escalation-approved',
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (evaluation.decision === 'deny') {
|
|
508
|
-
logAudit({ status: 'denied', error: evaluation.reason }, 0);
|
|
509
|
-
return {
|
|
510
|
-
content: [{ type: 'text', text: `DENIED: ${evaluation.reason}` }],
|
|
511
|
-
isError: true,
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
// Circuit breaker: deny if the same tool+args is called too many times
|
|
515
|
-
const cbVerdict = circuitBreaker.check(toolInfo.name, argsForTransport);
|
|
516
|
-
if (!cbVerdict.allowed) {
|
|
517
|
-
logAudit({ status: 'denied', error: cbVerdict.reason }, 0);
|
|
518
|
-
return {
|
|
519
|
-
content: [{ type: 'text', text: cbVerdict.reason }],
|
|
520
|
-
isError: true,
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
// Policy allows -- forward to the real MCP server with transport args
|
|
524
|
-
const startTime = Date.now();
|
|
525
|
-
try {
|
|
526
|
-
const clientState = clientStates.get(toolInfo.serverName);
|
|
527
|
-
if (!clientState) {
|
|
528
|
-
const err = `Internal error: no client connection for server "${toolInfo.serverName}"`;
|
|
529
|
-
logAudit({ status: 'denied', error: err }, 0);
|
|
530
|
-
return { content: [{ type: 'text', text: err }], isError: true };
|
|
531
|
-
}
|
|
532
|
-
const client = clientState.client;
|
|
533
|
-
// TODO(workaround): Remove once @cyanheads/git-mcp-server fixes outputSchema declarations.
|
|
534
|
-
//
|
|
535
|
-
// WHY: The git MCP server v2.8.4 declares outputSchema for tools like git_add and
|
|
536
|
-
// git_commit, but the structuredContent it actually returns does not match those schemas.
|
|
537
|
-
// The MCP SDK v1.26.0 Client.callTool() validates responses client-side against the
|
|
538
|
-
// declared outputSchema and throws McpError(-32602, "Structured content does not match
|
|
539
|
-
// the tool's output schema: ...") when there is a mismatch.
|
|
540
|
-
//
|
|
541
|
-
// WHAT: git_add declares required properties {success, stagedFiles, totalFiles, status}
|
|
542
|
-
// in its outputSchema, but actual responses (especially errors) are missing these and
|
|
543
|
-
// include additional undeclared properties. git_commit similarly requires {success,
|
|
544
|
-
// commitHash, author, timestamp, committedFiles, status} but returns different shapes.
|
|
545
|
-
//
|
|
546
|
-
// FIX: Passing CompatibilityCallToolResultSchema instead of the default
|
|
547
|
-
// CallToolResultSchema makes the response parsing more permissive, which avoids the
|
|
548
|
-
// client-side validation failure.
|
|
549
|
-
//
|
|
550
|
-
// CONSEQUENCE: By using CompatibilityCallToolResultSchema we lose client-side output
|
|
551
|
-
// validation for ALL MCP servers proxied through this path, not just the git server.
|
|
552
|
-
const result = await client.callTool({
|
|
553
|
-
name: toolInfo.name,
|
|
554
|
-
arguments: argsForTransport,
|
|
555
|
-
}, CompatibilityCallToolResultSchema, { timeout: getEscalationTimeoutMs() });
|
|
556
|
-
if (result.isError) {
|
|
557
|
-
const errorText = extractTextFromContent(result.content) ?? 'Unknown tool error';
|
|
558
|
-
const errorMessage = annotateSandboxViolation(errorText, serverIsSandboxed);
|
|
559
|
-
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
560
|
-
return { content: result.content, isError: true };
|
|
561
|
-
}
|
|
562
|
-
logAudit({ status: 'success' }, Date.now() - startTime);
|
|
563
|
-
return { content: result.content };
|
|
564
|
-
}
|
|
565
|
-
catch (err) {
|
|
566
|
-
const rawError = err instanceof Error ? err.message : String(err);
|
|
567
|
-
const errorMessage = annotateSandboxViolation(rawError, serverIsSandboxed);
|
|
568
|
-
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
569
|
-
return {
|
|
570
|
-
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
|
571
|
-
isError: true,
|
|
572
|
-
};
|
|
573
|
-
}
|
|
634
|
+
return handleCallTool(req.params.name, req.params.arguments ?? {}, callToolDeps);
|
|
574
635
|
});
|
|
575
|
-
//
|
|
576
|
-
const
|
|
577
|
-
const proxyTcpPort = process.env.PROXY_TCP_PORT;
|
|
636
|
+
// ── Transport selection ───────────────────────────────────────────
|
|
637
|
+
const transportConfig = selectTransportConfig();
|
|
578
638
|
let transport;
|
|
579
|
-
if (
|
|
580
|
-
const
|
|
581
|
-
if (!Number.isFinite(parsedPort) || parsedPort < 0 || parsedPort > 65535) {
|
|
582
|
-
throw new Error(`Invalid PROXY_TCP_PORT value "${proxyTcpPort}". Expected an integer between 0 and 65535.`);
|
|
583
|
-
}
|
|
584
|
-
// Bind to 0.0.0.0: Docker Desktop's VM needs to reach the proxy on
|
|
585
|
-
// the host, so loopback-only (127.0.0.1) would not be reachable from
|
|
586
|
-
// inside the container. Network-level egress restriction (--internal
|
|
587
|
-
// Docker network) limits which peers can connect.
|
|
588
|
-
const tcpTransport = new TcpServerTransport('0.0.0.0', parsedPort);
|
|
639
|
+
if (transportConfig.kind === 'tcp') {
|
|
640
|
+
const tcpTransport = new TcpServerTransport('0.0.0.0', transportConfig.port);
|
|
589
641
|
transport = tcpTransport;
|
|
590
|
-
// start() must be called before connect() to bind the port
|
|
591
642
|
await tcpTransport.start();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (portFilePath) {
|
|
595
|
-
writeFileSync(portFilePath, String(tcpTransport.port));
|
|
643
|
+
if (transportConfig.portFilePath) {
|
|
644
|
+
writeFileSync(transportConfig.portFilePath, String(tcpTransport.port));
|
|
596
645
|
}
|
|
597
646
|
if (sessionLogPath) {
|
|
598
|
-
logToSessionFile(sessionLogPath, `MCP proxy listening on
|
|
647
|
+
logToSessionFile(sessionLogPath, `MCP proxy listening on 0.0.0.0:${tcpTransport.port}`);
|
|
599
648
|
}
|
|
600
649
|
}
|
|
601
|
-
else if (
|
|
602
|
-
transport = new UdsServerTransport(
|
|
650
|
+
else if (transportConfig.kind === 'uds') {
|
|
651
|
+
transport = new UdsServerTransport(transportConfig.socketPath);
|
|
603
652
|
}
|
|
604
653
|
else {
|
|
605
654
|
transport = new StdioServerTransport();
|
|
606
655
|
}
|
|
607
656
|
await server.connect(transport);
|
|
608
|
-
//
|
|
609
|
-
// is spawned as a child by Code Mode and may receive either signal.
|
|
657
|
+
// ── Shutdown handler ──────────────────────────────────────────────
|
|
610
658
|
async function shutdown() {
|
|
611
659
|
for (const state of clientStates.values()) {
|
|
612
660
|
try {
|
|
@@ -633,8 +681,11 @@ async function main() {
|
|
|
633
681
|
void shutdown();
|
|
634
682
|
});
|
|
635
683
|
}
|
|
636
|
-
main()
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
});
|
|
684
|
+
// Only run main() when this module is the entry point (not when imported for testing)
|
|
685
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
686
|
+
main().catch((err) => {
|
|
687
|
+
process.stderr.write(`MCP Proxy Server fatal error: ${String(err)}\n`);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
640
691
|
//# sourceMappingURL=mcp-proxy-server.js.map
|