@provos/ironcurtain 0.10.0 → 0.12.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 +68 -14
- package/dist/auth/oauth-provider.js +1 -4
- package/dist/auth/oauth-provider.js.map +1 -1
- package/dist/cli-help.d.ts +12 -0
- package/dist/cli-help.js +21 -0
- package/dist/cli-help.js.map +1 -1
- package/dist/cli.js +46 -6
- package/dist/cli.js.map +1 -1
- package/dist/config/agent-model-guard.d.ts +14 -0
- package/dist/config/agent-model-guard.js +24 -0
- package/dist/config/agent-model-guard.js.map +1 -0
- package/dist/config/config-command.js +277 -7
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/first-start.js +62 -7
- package/dist/config/first-start.js.map +1 -1
- package/dist/config/generated/compiled-policy.json +528 -161
- package/dist/config/generated/test-scenarios.json +4810 -170
- package/dist/config/generated/tool-annotations.json +34 -47
- package/dist/config/index.d.ts +58 -0
- package/dist/config/index.js +97 -15
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +2 -2
- package/dist/config/model-provider.d.ts +9 -0
- package/dist/config/model-provider.js +13 -0
- package/dist/config/model-provider.js.map +1 -1
- package/dist/config/paths.d.ts +217 -0
- package/dist/config/paths.js +363 -14
- package/dist/config/paths.js.map +1 -1
- package/dist/config/types.d.ts +7 -1
- package/dist/config/user-config.d.ts +101 -6
- package/dist/config/user-config.js +145 -31
- package/dist/config/user-config.js.map +1 -1
- package/dist/config/validate-policy-dir.d.ts +10 -0
- package/dist/config/validate-policy-dir.js +59 -0
- package/dist/config/validate-policy-dir.js.map +1 -0
- package/dist/cron/job-commands.js +22 -2
- package/dist/cron/job-commands.js.map +1 -1
- package/dist/cron/types.d.ts +16 -2
- package/dist/cron/types.js +1 -3
- package/dist/cron/types.js.map +1 -1
- package/dist/daemon/daemon-command.js +30 -7
- package/dist/daemon/daemon-command.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.d.ts +62 -1
- package/dist/daemon/ironcurtain-daemon.js +231 -8
- package/dist/daemon/ironcurtain-daemon.js.map +1 -1
- package/dist/daemon-client/daemon-client.d.ts +123 -0
- package/dist/daemon-client/daemon-client.js +324 -0
- package/dist/daemon-client/daemon-client.js.map +1 -0
- package/dist/docker/adapters/claude-code.js +194 -20
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/adapters/codex.d.ts +9 -0
- package/dist/docker/adapters/codex.js +297 -0
- package/dist/docker/adapters/codex.js.map +1 -0
- package/dist/docker/adapters/goose.d.ts +12 -11
- package/dist/docker/adapters/goose.js +80 -27
- package/dist/docker/adapters/goose.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +183 -5
- package/dist/docker/agent-adapter.js +16 -0
- package/dist/docker/agent-adapter.js.map +1 -1
- package/dist/docker/agent-registry.js +5 -0
- package/dist/docker/agent-registry.js.map +1 -1
- package/dist/docker/apple-container-manager.d.ts +49 -0
- package/dist/docker/apple-container-manager.js +464 -0
- package/dist/docker/apple-container-manager.js.map +1 -0
- package/dist/docker/code-mode-proxy.d.ts +38 -0
- package/dist/docker/code-mode-proxy.js +6 -1
- package/dist/docker/code-mode-proxy.js.map +1 -1
- package/dist/docker/container-lifecycle.d.ts +2 -2
- package/dist/docker/container-lifecycle.js.map +1 -1
- package/dist/docker/container-runtime.d.ts +29 -0
- package/dist/docker/container-runtime.js +76 -0
- package/dist/docker/container-runtime.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +113 -62
- package/dist/docker/docker-agent-session.js +271 -294
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +428 -26
- package/dist/docker/docker-infrastructure.js +1005 -70
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.d.ts +58 -3
- package/dist/docker/docker-manager.js +378 -33
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/docker-probe.d.ts +45 -0
- package/dist/docker/docker-probe.js +86 -0
- package/dist/docker/docker-probe.js.map +1 -0
- package/dist/docker/docker-progress-sink.d.ts +43 -0
- package/dist/docker/docker-progress-sink.js +299 -0
- package/dist/docker/docker-progress-sink.js.map +1 -0
- package/dist/docker/mitm-proxy.d.ts +135 -1
- package/dist/docker/mitm-proxy.js +611 -25
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/network-topology.d.ts +81 -0
- package/dist/docker/network-topology.js +160 -0
- package/dist/docker/network-topology.js.map +1 -0
- package/dist/docker/oauth-credentials.d.ts +59 -3
- package/dist/docker/oauth-credentials.js +181 -14
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/oauth-token-manager.js +2 -2
- package/dist/docker/oauth-token-manager.js.map +1 -1
- package/dist/docker/orientation.d.ts +1 -1
- package/dist/docker/package-types.d.ts +1 -1
- package/dist/docker/provider-config.d.ts +78 -3
- package/dist/docker/provider-config.js +295 -31
- package/dist/docker/provider-config.js.map +1 -1
- package/dist/docker/provision-lock.d.ts +46 -0
- package/dist/docker/provision-lock.js +152 -0
- package/dist/docker/provision-lock.js.map +1 -0
- package/dist/docker/proxy-tools.d.ts +0 -2
- package/dist/docker/proxy-tools.js +2 -7
- package/dist/docker/proxy-tools.js.map +1 -1
- package/dist/docker/pty-session.d.ts +64 -1
- package/dist/docker/pty-session.js +306 -104
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/docker/pty-types.d.ts +0 -2
- package/dist/docker/pty-types.js +0 -2
- package/dist/docker/pty-types.js.map +1 -1
- package/dist/docker/registry-proxy.d.ts +46 -0
- package/dist/docker/registry-proxy.js +255 -1
- package/dist/docker/registry-proxy.js.map +1 -1
- package/dist/docker/resource-limits.d.ts +134 -0
- package/dist/docker/resource-limits.js +203 -0
- package/dist/docker/resource-limits.js.map +1 -0
- package/dist/docker/spawn-with-idle-timeout.d.ts +39 -0
- package/dist/docker/spawn-with-idle-timeout.js +152 -0
- package/dist/docker/spawn-with-idle-timeout.js.map +1 -0
- package/dist/docker/sse-extractor.d.ts +51 -0
- package/dist/docker/sse-extractor.js +303 -0
- package/dist/docker/sse-extractor.js.map +1 -0
- package/dist/docker/token-stream-bus.d.ts +83 -0
- package/dist/docker/token-stream-bus.js +98 -0
- package/dist/docker/token-stream-bus.js.map +1 -0
- package/dist/docker/token-stream-types.d.ts +71 -0
- package/dist/docker/token-stream-types.js +8 -0
- package/dist/docker/token-stream-types.js.map +1 -0
- package/dist/docker/trajectory-capture.d.ts +79 -0
- package/dist/docker/trajectory-capture.js +521 -0
- package/dist/docker/trajectory-capture.js.map +1 -0
- package/dist/docker/trajectory-reassembler.d.ts +286 -0
- package/dist/docker/trajectory-reassembler.js +990 -0
- package/dist/docker/trajectory-reassembler.js.map +1 -0
- package/dist/docker/trajectory-tap.d.ts +100 -0
- package/dist/docker/trajectory-tap.js +410 -0
- package/dist/docker/trajectory-tap.js.map +1 -0
- package/dist/docker/trajectory-types.d.ts +187 -0
- package/dist/docker/trajectory-types.js +43 -0
- package/dist/docker/trajectory-types.js.map +1 -0
- package/dist/docker/types.d.ts +105 -12
- package/dist/doctor/checks.d.ts +171 -0
- package/dist/doctor/checks.js +598 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/doctor/doctor-command.d.ts +29 -0
- package/dist/doctor/doctor-command.js +190 -0
- package/dist/doctor/doctor-command.js.map +1 -0
- package/dist/doctor/mcp-liveness.d.ts +28 -0
- package/dist/doctor/mcp-liveness.js +92 -0
- package/dist/doctor/mcp-liveness.js.map +1 -0
- package/dist/doctor/oauth-checks.d.ts +23 -0
- package/dist/doctor/oauth-checks.js +157 -0
- package/dist/doctor/oauth-checks.js.map +1 -0
- package/dist/doctor/output.d.ts +26 -0
- package/dist/doctor/output.js +82 -0
- package/dist/doctor/output.js.map +1 -0
- package/dist/escalation/listener-command.js +0 -3
- package/dist/escalation/listener-command.js.map +1 -1
- package/dist/event-bus/typed-event-bus.d.ts +26 -0
- package/dist/event-bus/typed-event-bus.js +35 -0
- package/dist/event-bus/typed-event-bus.js.map +1 -0
- package/dist/index.js +54 -21
- package/dist/index.js.map +1 -1
- package/dist/logger.js +31 -2
- package/dist/logger.js.map +1 -1
- package/dist/memory/auto-save.d.ts +18 -4
- package/dist/memory/auto-save.js +46 -6
- package/dist/memory/auto-save.js.map +1 -1
- package/dist/memory/memory-policy.d.ts +41 -0
- package/dist/memory/memory-policy.js +36 -0
- package/dist/memory/memory-policy.js.map +1 -0
- package/dist/mux/mux-app.d.ts +2 -0
- package/dist/mux/mux-app.js +1 -0
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.d.ts +10 -1
- package/dist/mux/mux-command.js +74 -40
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-renderer.js +2 -12
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/pty-bridge.d.ts +2 -0
- package/dist/mux/pty-bridge.js +3 -0
- package/dist/mux/pty-bridge.js.map +1 -1
- package/dist/mux/session-scanner.js +1 -2
- package/dist/mux/session-scanner.js.map +1 -1
- package/dist/{pipeline → observability}/llm-logger.d.ts +8 -3
- package/dist/{pipeline → observability}/llm-logger.js +8 -3
- package/dist/observability/llm-logger.js.map +1 -0
- package/dist/observe/observe-command.d.ts +12 -0
- package/dist/observe/observe-command.js +285 -0
- package/dist/observe/observe-command.js.map +1 -0
- package/dist/observe/observe-renderer.d.ts +24 -0
- package/dist/observe/observe-renderer.js +87 -0
- package/dist/observe/observe-renderer.js.map +1 -0
- package/dist/observe/observe-tui-rain.d.ts +42 -0
- package/dist/observe/observe-tui-rain.js +531 -0
- package/dist/observe/observe-tui-rain.js.map +1 -0
- package/dist/observe/observe-tui-text-panel.d.ts +46 -0
- package/dist/observe/observe-tui-text-panel.js +525 -0
- package/dist/observe/observe-tui-text-panel.js.map +1 -0
- package/dist/observe/observe-tui-types.d.ts +270 -0
- package/dist/observe/observe-tui-types.js +200 -0
- package/dist/observe/observe-tui-types.js.map +1 -0
- package/dist/observe/observe-tui-word-scorer.d.ts +143 -0
- package/dist/observe/observe-tui-word-scorer.js +572 -0
- package/dist/observe/observe-tui-word-scorer.js.map +1 -0
- package/dist/observe/observe-tui.d.ts +59 -0
- package/dist/observe/observe-tui.js +592 -0
- package/dist/observe/observe-tui.js.map +1 -0
- package/dist/persona/broad-policy-validator.d.ts +95 -0
- package/dist/persona/broad-policy-validator.js +171 -0
- package/dist/persona/broad-policy-validator.js.map +1 -0
- package/dist/persona/compile-persona-policy.d.ts +54 -2
- package/dist/persona/compile-persona-policy.js +29 -4
- package/dist/persona/compile-persona-policy.js.map +1 -1
- package/dist/persona/event-bus-progress-reporter.d.ts +57 -0
- package/dist/persona/event-bus-progress-reporter.js +66 -0
- package/dist/persona/event-bus-progress-reporter.js.map +1 -0
- package/dist/persona/memory-gate.d.ts +19 -0
- package/dist/persona/memory-gate.js +33 -0
- package/dist/persona/memory-gate.js.map +1 -0
- package/dist/persona/persona-command.js +77 -72
- package/dist/persona/persona-command.js.map +1 -1
- package/dist/persona/persona-compile-orchestrator.d.ts +101 -0
- package/dist/persona/persona-compile-orchestrator.js +504 -0
- package/dist/persona/persona-compile-orchestrator.js.map +1 -0
- package/dist/persona/persona-service.d.ts +137 -0
- package/dist/persona/persona-service.js +263 -0
- package/dist/persona/persona-service.js.map +1 -0
- package/dist/persona/policy-mutation-audit.d.ts +109 -0
- package/dist/persona/policy-mutation-audit.js +179 -0
- package/dist/persona/policy-mutation-audit.js.map +1 -0
- package/dist/persona/resolve.d.ts +22 -0
- package/dist/persona/resolve.js +45 -2
- package/dist/persona/resolve.js.map +1 -1
- package/dist/persona/rule-delta.d.ts +30 -0
- package/dist/persona/rule-delta.js +82 -0
- package/dist/persona/rule-delta.js.map +1 -0
- package/dist/persona/types.d.ts +28 -0
- package/dist/pipeline/annotate.js +10 -2
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.js +14 -2
- package/dist/pipeline/compile.js.map +1 -1
- package/dist/pipeline/constitution-compiler.d.ts +25 -14
- package/dist/pipeline/constitution-compiler.js +107 -38
- package/dist/pipeline/constitution-compiler.js.map +1 -1
- package/dist/pipeline/dynamic-list-types.d.ts +3 -6
- package/dist/pipeline/dynamic-list-types.js +3 -16
- package/dist/pipeline/dynamic-list-types.js.map +1 -1
- package/dist/pipeline/pipeline-runner.d.ts +63 -4
- package/dist/pipeline/pipeline-runner.js +242 -94
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +33 -4
- package/dist/pipeline/pipeline-shared.js +79 -19
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/policy-verifier.js +16 -3
- package/dist/pipeline/policy-verifier.js.map +1 -1
- package/dist/pipeline/proxy-mcp-connections.js +17 -2
- package/dist/pipeline/proxy-mcp-connections.js.map +1 -1
- package/dist/pipeline/refresh-lists.js +3 -5
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/pipeline/scenario-generator.d.ts +4 -8
- package/dist/pipeline/scenario-generator.js +13 -46
- package/dist/pipeline/scenario-generator.js.map +1 -1
- package/dist/sandbox/index.d.ts +29 -0
- package/dist/sandbox/index.js +351 -119
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/ironcurtain-protocol.d.ts +52 -0
- package/dist/sandbox/ironcurtain-protocol.js +185 -0
- package/dist/sandbox/ironcurtain-protocol.js.map +1 -0
- package/dist/session/agent-session.d.ts +6 -0
- package/dist/session/agent-session.js +16 -5
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/cli-transport.js +0 -6
- package/dist/session/cli-transport.js.map +1 -1
- package/dist/session/index.d.ts +35 -14
- package/dist/session/index.js +333 -130
- package/dist/session/index.js.map +1 -1
- package/dist/session/preflight.d.ts +21 -13
- package/dist/session/preflight.js +227 -88
- package/dist/session/preflight.js.map +1 -1
- package/dist/session/prompts.d.ts +2 -5
- package/dist/session/prompts.js.map +1 -1
- package/dist/session/session-manager.d.ts +61 -6
- package/dist/session/session-manager.js +72 -10
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/session-metadata.d.ts +21 -5
- package/dist/session/session-metadata.js +34 -12
- package/dist/session/session-metadata.js.map +1 -1
- package/dist/session/types.d.ts +320 -5
- package/dist/session/types.js +57 -1
- package/dist/session/types.js.map +1 -1
- package/dist/signal/bot-command.js +6 -1
- package/dist/signal/bot-command.js.map +1 -1
- package/dist/signal/setup-signal.js +13 -21
- package/dist/signal/setup-signal.js.map +1 -1
- package/dist/signal/signal-bot-daemon.d.ts +16 -1
- package/dist/signal/signal-bot-daemon.js +39 -27
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/signal/signal-container.d.ts +3 -3
- package/dist/signal/signal-container.js +9 -3
- package/dist/signal/signal-container.js.map +1 -1
- package/dist/skills/discovery.d.ts +77 -0
- package/dist/skills/discovery.js +206 -0
- package/dist/skills/discovery.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +3 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/staging.d.ts +28 -0
- package/dist/skills/staging.js +68 -0
- package/dist/skills/staging.js.map +1 -0
- package/dist/skills/types.d.ts +46 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/trusted-process/approval-whitelist.d.ts +0 -2
- package/dist/trusted-process/approval-whitelist.js +17 -17
- package/dist/trusted-process/approval-whitelist.js.map +1 -1
- package/dist/trusted-process/async-mutex.d.ts +32 -0
- package/dist/trusted-process/async-mutex.js +50 -0
- package/dist/trusted-process/async-mutex.js.map +1 -0
- package/dist/trusted-process/audit-log.d.ts +38 -1
- package/dist/trusted-process/audit-log.js +82 -13
- package/dist/trusted-process/audit-log.js.map +1 -1
- package/dist/trusted-process/call-circuit-breaker.d.ts +6 -0
- package/dist/trusted-process/call-circuit-breaker.js +23 -3
- package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
- package/dist/trusted-process/container-command.d.ts +35 -0
- package/dist/trusted-process/container-command.js +49 -0
- package/dist/trusted-process/container-command.js.map +1 -0
- package/dist/trusted-process/control-server.d.ts +101 -0
- package/dist/trusted-process/control-server.js +282 -0
- package/dist/trusted-process/control-server.js.map +1 -0
- package/dist/trusted-process/domain-utils.d.ts +6 -6
- package/dist/trusted-process/domain-utils.js +7 -23
- package/dist/trusted-process/domain-utils.js.map +1 -1
- package/dist/trusted-process/error-prefixes.d.ts +16 -0
- package/dist/trusted-process/error-prefixes.js +17 -0
- package/dist/trusted-process/error-prefixes.js.map +1 -0
- package/dist/trusted-process/index.d.ts +40 -23
- package/dist/trusted-process/index.js +131 -225
- package/dist/trusted-process/index.js.map +1 -1
- package/dist/trusted-process/list-matcher.d.ts +17 -0
- package/dist/trusted-process/list-matcher.js +31 -0
- package/dist/trusted-process/list-matcher.js.map +1 -0
- package/dist/trusted-process/mcp-client-manager.d.ts +35 -6
- package/dist/trusted-process/mcp-client-manager.js +40 -45
- package/dist/trusted-process/mcp-client-manager.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.d.ts +31 -120
- package/dist/trusted-process/mcp-proxy-server.js +201 -660
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.d.ts +16 -4
- package/dist/trusted-process/policy-engine.js +103 -47
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/policy-roots.d.ts +6 -0
- package/dist/trusted-process/policy-roots.js +20 -0
- package/dist/trusted-process/policy-roots.js.map +1 -1
- package/dist/trusted-process/sandbox-integration.d.ts +12 -0
- package/dist/trusted-process/sandbox-integration.js +18 -0
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/tcp-server-transport.d.ts +12 -1
- package/dist/trusted-process/tcp-server-transport.js +9 -1
- package/dist/trusted-process/tcp-server-transport.js.map +1 -1
- package/dist/trusted-process/tool-call-coordinator.d.ts +283 -0
- package/dist/trusted-process/tool-call-coordinator.js +592 -0
- package/dist/trusted-process/tool-call-coordinator.js.map +1 -0
- package/dist/trusted-process/tool-call-pipeline.d.ts +213 -0
- package/dist/trusted-process/tool-call-pipeline.js +832 -0
- package/dist/trusted-process/tool-call-pipeline.js.map +1 -0
- package/dist/trusted-process/tool-description-hints.d.ts +1 -1
- package/dist/types/argument-roles.d.ts +1 -1
- package/dist/types/argument-roles.js +14 -1
- package/dist/types/argument-roles.js.map +1 -1
- package/dist/types/audit.d.ts +9 -0
- package/dist/{session → types}/errors.d.ts +8 -2
- package/dist/{session → types}/errors.js +8 -2
- package/dist/types/errors.js.map +1 -0
- package/dist/types/reserved-servers.d.ts +11 -0
- package/dist/types/reserved-servers.js +12 -0
- package/dist/types/reserved-servers.js.map +1 -0
- package/dist/types/server-listing.d.ts +14 -0
- package/dist/types/server-listing.js +2 -0
- package/dist/types/server-listing.js.map +1 -0
- package/dist/types/slug.d.ts +2 -0
- package/dist/types/slug.js +2 -0
- package/dist/types/slug.js.map +1 -1
- package/dist/types/url-normalize.d.ts +16 -0
- package/dist/types/url-normalize.js +33 -0
- package/dist/types/url-normalize.js.map +1 -0
- package/dist/util/atomic-write.d.ts +31 -0
- package/dist/util/atomic-write.js +43 -0
- package/dist/util/atomic-write.js.map +1 -0
- package/dist/utils/error-message.d.ts +8 -0
- package/dist/utils/error-message.js +11 -0
- package/dist/utils/error-message.js.map +1 -0
- package/dist/utils/exec-error.d.ts +19 -0
- package/dist/utils/exec-error.js +16 -0
- package/dist/utils/exec-error.js.map +1 -0
- package/dist/utils/is-plain-object.d.ts +7 -0
- package/dist/utils/is-plain-object.js +12 -0
- package/dist/utils/is-plain-object.js.map +1 -0
- package/dist/utils/preflight-checks.d.ts +16 -0
- package/dist/utils/preflight-checks.js +178 -0
- package/dist/utils/preflight-checks.js.map +1 -0
- package/dist/web-ui/__tests__/json-rpc-dispatch.test.d.ts +7 -0
- package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +725 -0
- package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -0
- package/dist/web-ui/conn-registry.d.ts +14 -0
- package/dist/web-ui/conn-registry.js +25 -0
- package/dist/web-ui/conn-registry.js.map +1 -0
- package/dist/web-ui/dispatch/escalation-dispatch.d.ts +7 -0
- package/dist/web-ui/dispatch/escalation-dispatch.js +52 -0
- package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/job-dispatch.d.ts +8 -0
- package/dist/web-ui/dispatch/job-dispatch.js +83 -0
- package/dist/web-ui/dispatch/job-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/persona-dispatch.d.ts +50 -0
- package/dist/web-ui/dispatch/persona-dispatch.js +269 -0
- package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/session-dispatch.d.ts +9 -0
- package/dist/web-ui/dispatch/session-dispatch.js +219 -0
- package/dist/web-ui/dispatch/session-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/token-stream-dispatch.d.ts +15 -0
- package/dist/web-ui/dispatch/token-stream-dispatch.js +55 -0
- package/dist/web-ui/dispatch/token-stream-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/types.d.ts +54 -0
- package/dist/web-ui/dispatch/types.js +72 -0
- package/dist/web-ui/dispatch/types.js.map +1 -0
- package/dist/web-ui/dispatch/workflow-dispatch.d.ts +103 -0
- package/dist/web-ui/dispatch/workflow-dispatch.js +1036 -0
- package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -0
- package/dist/web-ui/json-rpc-dispatch.d.ts +12 -0
- package/dist/web-ui/json-rpc-dispatch.js +34 -0
- package/dist/web-ui/json-rpc-dispatch.js.map +1 -0
- package/dist/web-ui/state-graph.d.ts +13 -0
- package/dist/web-ui/state-graph.js +82 -0
- package/dist/web-ui/state-graph.js.map +1 -0
- package/dist/web-ui/token-stream-bridge.d.ts +76 -0
- package/dist/web-ui/token-stream-bridge.js +265 -0
- package/dist/web-ui/token-stream-bridge.js.map +1 -0
- package/dist/web-ui/web-event-bus.d.ts +176 -0
- package/dist/web-ui/web-event-bus.js +19 -0
- package/dist/web-ui/web-event-bus.js.map +1 -0
- package/dist/web-ui/web-session-transport.d.ts +31 -0
- package/dist/web-ui/web-session-transport.js +109 -0
- package/dist/web-ui/web-session-transport.js.map +1 -0
- package/dist/web-ui/web-ui-server.d.ts +90 -0
- package/dist/web-ui/web-ui-server.js +477 -0
- package/dist/web-ui/web-ui-server.js.map +1 -0
- package/dist/web-ui/web-ui-types.d.ts +418 -0
- package/dist/web-ui/web-ui-types.js +32 -0
- package/dist/web-ui/web-ui-types.js.map +1 -0
- package/dist/web-ui/ws-utils.d.ts +5 -0
- package/dist/web-ui/ws-utils.js +12 -0
- package/dist/web-ui/ws-utils.js.map +1 -0
- package/dist/web-ui-static/assets/index-BD5U6ILh.css +1 -0
- package/dist/web-ui-static/assets/index-FMsHWEfK.js +79 -0
- package/dist/web-ui-static/assets/orbitron-latin-700-normal-4jsRXGGJ.woff2 +0 -0
- package/dist/web-ui-static/assets/orbitron-latin-700-normal-DmYOh-W9.woff +0 -0
- package/dist/web-ui-static/index.html +14 -0
- package/dist/workflow/artifacts.d.ts +59 -0
- package/dist/workflow/artifacts.js +188 -0
- package/dist/workflow/artifacts.js.map +1 -0
- package/dist/workflow/checkpoint-selection.d.ts +34 -0
- package/dist/workflow/checkpoint-selection.js +48 -0
- package/dist/workflow/checkpoint-selection.js.map +1 -0
- package/dist/workflow/checkpoint.d.ts +35 -0
- package/dist/workflow/checkpoint.js +84 -0
- package/dist/workflow/checkpoint.js.map +1 -0
- package/dist/workflow/cli-shared.d.ts +62 -0
- package/dist/workflow/cli-shared.js +117 -0
- package/dist/workflow/cli-shared.js.map +1 -0
- package/dist/workflow/cli-support.d.ts +92 -0
- package/dist/workflow/cli-support.js +421 -0
- package/dist/workflow/cli-support.js.map +1 -0
- package/dist/workflow/container-snapshots.d.ts +29 -0
- package/dist/workflow/container-snapshots.js +137 -0
- package/dist/workflow/container-snapshots.js.map +1 -0
- package/dist/workflow/daemon-gate-commands.d.ts +21 -0
- package/dist/workflow/daemon-gate-commands.js +599 -0
- package/dist/workflow/daemon-gate-commands.js.map +1 -0
- package/dist/workflow/definition-loader.d.ts +32 -0
- package/dist/workflow/definition-loader.js +56 -0
- package/dist/workflow/definition-loader.js.map +1 -0
- package/dist/workflow/discovery.d.ts +107 -0
- package/dist/workflow/discovery.js +263 -0
- package/dist/workflow/discovery.js.map +1 -0
- package/dist/workflow/errors.d.ts +66 -0
- package/dist/workflow/errors.js +49 -0
- package/dist/workflow/errors.js.map +1 -0
- package/dist/workflow/guards.d.ts +8 -0
- package/dist/workflow/guards.js +38 -0
- package/dist/workflow/guards.js.map +1 -0
- package/dist/workflow/lane-template.d.ts +89 -0
- package/dist/workflow/lane-template.js +141 -0
- package/dist/workflow/lane-template.js.map +1 -0
- package/dist/workflow/lint-integration.d.ts +64 -0
- package/dist/workflow/lint-integration.js +56 -0
- package/dist/workflow/lint-integration.js.map +1 -0
- package/dist/workflow/lint.d.ts +72 -0
- package/dist/workflow/lint.js +593 -0
- package/dist/workflow/lint.js.map +1 -0
- package/dist/workflow/machine-builder.d.ts +178 -0
- package/dist/workflow/machine-builder.js +678 -0
- package/dist/workflow/machine-builder.js.map +1 -0
- package/dist/workflow/message-log.d.ts +99 -0
- package/dist/workflow/message-log.js +40 -0
- package/dist/workflow/message-log.js.map +1 -0
- package/dist/workflow/orchestrator.d.ts +954 -0
- package/dist/workflow/orchestrator.js +3594 -0
- package/dist/workflow/orchestrator.js.map +1 -0
- package/dist/workflow/prompt-builder.d.ts +39 -0
- package/dist/workflow/prompt-builder.js +243 -0
- package/dist/workflow/prompt-builder.js.map +1 -0
- package/dist/workflow/run-state-command.d.ts +9 -0
- package/dist/workflow/run-state-command.js +414 -0
- package/dist/workflow/run-state-command.js.map +1 -0
- package/dist/workflow/run-state-debug-capture.d.ts +40 -0
- package/dist/workflow/run-state-debug-capture.js +101 -0
- package/dist/workflow/run-state-debug-capture.js.map +1 -0
- package/dist/workflow/status-parser.d.ts +68 -0
- package/dist/workflow/status-parser.js +274 -0
- package/dist/workflow/status-parser.js.map +1 -0
- package/dist/workflow/types.d.ts +598 -0
- package/dist/workflow/types.js +85 -0
- package/dist/workflow/types.js.map +1 -0
- package/dist/workflow/validate.d.ts +80 -0
- package/dist/workflow/validate.js +788 -0
- package/dist/workflow/validate.js.map +1 -0
- package/dist/workflow/workflow-command.d.ts +9 -0
- package/dist/workflow/workflow-command.js +670 -0
- package/dist/workflow/workflow-command.js.map +1 -0
- package/dist/workflow/workflow-discovery.d.ts +27 -0
- package/dist/workflow/workflow-discovery.js +121 -0
- package/dist/workflow/workflow-discovery.js.map +1 -0
- package/dist/workflow/workflow-manager.d.ts +156 -0
- package/dist/workflow/workflow-manager.js +359 -0
- package/dist/workflow/workflow-manager.js.map +1 -0
- package/dist/workflow/workflows/design-and-code/README.md +50 -0
- package/dist/workflow/workflows/design-and-code/workflow.yaml +169 -0
- package/dist/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
- package/dist/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
- package/dist/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
- package/dist/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
- package/dist/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
- package/dist/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
- package/dist/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
- package/dist/workflow/workflows/evolve/scripts/LICENSE +201 -0
- package/dist/workflow/workflows/evolve/scripts/README.md +314 -0
- package/dist/workflow/workflows/evolve/scripts/evolve-brief +16 -0
- package/dist/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
- package/dist/workflow/workflows/evolve/scripts/evolve-db +16 -0
- package/dist/workflow/workflows/evolve/scripts/evolve-eval +16 -0
- package/dist/workflow/workflows/evolve/scripts/evolve-files +16 -0
- package/dist/workflow/workflows/evolve/scripts/evolve-summary +16 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
- package/dist/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
- package/dist/workflow/workflows/evolve/scripts/requirements.txt +2 -0
- package/dist/workflow/workflows/evolve/workflow.yaml +646 -0
- package/dist/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
- package/dist/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
- package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
- package/dist/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
- package/dist/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
- package/dist/workflow/workflows/test-email-summary/workflow.yaml +90 -0
- package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +206 -0
- package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +167 -0
- package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +189 -0
- package/dist/workflow/workflows/vuln-discovery/workflow.yaml +925 -0
- package/dist/workflow/worktree.d.ts +43 -0
- package/dist/workflow/worktree.js +106 -0
- package/dist/workflow/worktree.js.map +1 -0
- package/docker/Dockerfile.base +31 -0
- package/docker/Dockerfile.base.arm64 +27 -0
- package/docker/Dockerfile.claude-code +3 -1
- package/docker/Dockerfile.codex +17 -0
- package/docker/Dockerfile.goose +3 -1
- package/docker/entrypoint-claude-code.sh +7 -0
- package/docker/entrypoint-codex.sh +42 -0
- package/docker/entrypoint-goose.sh +6 -0
- package/docker/entrypoint-uid-remap.sh +93 -0
- package/package.json +27 -11
- package/packages/web-ui/README.md +113 -0
- package/packages/web-ui/docs/workflow-state-machine.png +0 -0
- package/src/config/generated/compiled-policy.json +528 -161
- package/src/config/generated/test-scenarios.json +4810 -170
- package/src/config/generated/tool-annotations.json +34 -47
- package/src/config/mcp-servers.json +2 -2
- package/src/workflow/workflows/design-and-code/README.md +50 -0
- package/src/workflow/workflows/design-and-code/workflow.yaml +169 -0
- package/src/workflow/workflows/deterministic-eval-smoke/scripts/format_report.js +29 -0
- package/src/workflow/workflows/deterministic-eval-smoke/scripts/package.json +5 -0
- package/src/workflow/workflows/deterministic-eval-smoke/scripts/requirements.txt +1 -0
- package/src/workflow/workflows/deterministic-eval-smoke/scripts/run_eval.py +41 -0
- package/src/workflow/workflows/deterministic-eval-smoke/workflow.yaml +57 -0
- package/src/workflow/workflows/deterministic-verdict-smoke/scripts/classify.py +18 -0
- package/src/workflow/workflows/deterministic-verdict-smoke/workflow.yaml +51 -0
- package/src/workflow/workflows/evolve/scripts/LICENSE +201 -0
- package/src/workflow/workflows/evolve/scripts/README.md +314 -0
- package/src/workflow/workflows/evolve/scripts/evolve-brief +16 -0
- package/src/workflow/workflows/evolve/scripts/evolve-cognition +16 -0
- package/src/workflow/workflows/evolve/scripts/evolve-db +16 -0
- package/src/workflow/workflows/evolve/scripts/evolve-eval +16 -0
- package/src/workflow/workflows/evolve/scripts/evolve-files +16 -0
- package/src/workflow/workflows/evolve/scripts/evolve-summary +16 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/__init__.py +14 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/__init__.py +17 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/base.py +21 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/factory.py +90 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/greedy.py +17 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/island.py +316 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/random.py +18 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/algorithms/ucb1.py +55 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/best_snapshot.py +62 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/cli.py +752 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/cognition.py +127 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/database.py +218 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/diff.py +57 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/embedding.py +84 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/file_lock.py +45 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/run_state.py +343 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/sampling_config.py +173 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/structures.py +93 -0
- package/src/workflow/workflows/evolve/scripts/evolve_core/vector_index.py +191 -0
- package/src/workflow/workflows/evolve/scripts/evolve_result.py +1503 -0
- package/src/workflow/workflows/evolve/scripts/requirements.txt +2 -0
- package/src/workflow/workflows/evolve/workflow.yaml +646 -0
- package/src/workflow/workflows/test-email-summary/skills/email-formatting/SKILL.md +55 -0
- package/src/workflow/workflows/test-email-summary/skills/gmail-query-syntax/SKILL.md +63 -0
- package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/SKILL.md +41 -0
- package/src/workflow/workflows/test-email-summary/skills/json-schema-emails/schema.json +34 -0
- package/src/workflow/workflows/test-email-summary/skills/summary-style-guide/SKILL.md +51 -0
- package/src/workflow/workflows/test-email-summary/workflow.yaml +90 -0
- package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +206 -0
- package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +167 -0
- package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +189 -0
- package/src/workflow/workflows/vuln-discovery/workflow.yaml +925 -0
- package/dist/pipeline/llm-logger.js.map +0 -1
- package/dist/session/errors.js.map +0 -1
|
@@ -1,65 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP Proxy Server --
|
|
2
|
+
* MCP Proxy Server -- Standalone MCP subprocess for Code Mode.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Runs as a child process spawned by MCPClientManager. Acts as a pure
|
|
5
|
+
* MCP pass-through relay: connects to real MCP backend servers and
|
|
6
|
+
* forwards tool calls without any policy evaluation. The coordinator
|
|
7
|
+
* in the parent process owns the full security pipeline.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* - MCP transport setup (stdio/UDS/TCP)
|
|
11
|
+
* - Backend MCP server connection (StdioClientTransport)
|
|
12
|
+
* - OAuth credential injection and TokenFileRefresher
|
|
13
|
+
* - Sandbox-runtime wrapping (srt)
|
|
14
|
+
* - ListTools handler (raw pass-through)
|
|
15
|
+
* - CallTool handler (pure forward to backend)
|
|
16
|
+
* - Virtual proxy tools (MITM_CONTROL_ADDR)
|
|
17
|
+
* - Tool description hints injection
|
|
18
|
+
* - Roots bridging: on `notifications/roots/list_changed` from the
|
|
19
|
+
* coordinator, fetch fresh roots via `server.listRoots()`, update
|
|
20
|
+
* the shared `relayRoots` array, and notify each backend client so
|
|
21
|
+
* they re-query their `roots/list` handlers with the new set.
|
|
12
22
|
*
|
|
13
23
|
* Configuration via environment variables:
|
|
14
|
-
* AUDIT_LOG_PATH -- path to the audit log file
|
|
15
24
|
* MCP_SERVERS_CONFIG -- JSON string of MCP server configs to proxy
|
|
16
25
|
* GENERATED_DIR -- path to the generated artifacts directory
|
|
17
26
|
* PROTECTED_PATHS -- JSON array of protected paths
|
|
18
|
-
* ALLOWED_DIRECTORY -- (optional) sandbox directory
|
|
19
|
-
* ESCALATION_DIR -- (optional) directory for file-based escalation IPC
|
|
27
|
+
* ALLOWED_DIRECTORY -- (optional) sandbox directory
|
|
20
28
|
* SESSION_LOG_PATH -- (optional) path for capturing child process stderr
|
|
21
29
|
* SANDBOX_POLICY -- (optional) "enforce" | "warn" (default: "warn")
|
|
22
|
-
*
|
|
23
|
-
* SERVER_FILTER -- (optional) when set, only connect to this single server name
|
|
24
|
-
* AUTO_APPROVE_ENABLED -- (optional) "true" to enable auto-approval of escalations
|
|
25
|
-
* AUTO_APPROVE_MODEL_ID -- (optional) qualified model ID for auto-approve LLM
|
|
26
|
-
* AUTO_APPROVE_API_KEY -- (optional) API key for the auto-approve model's provider
|
|
27
|
-
* AUTO_APPROVE_LLM_LOG_PATH -- (optional) path to JSONL file for auto-approve LLM logging
|
|
30
|
+
* SERVER_FILTER -- (optional) only connect to this single server name
|
|
28
31
|
*/
|
|
29
32
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
30
33
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
31
34
|
import { UdsServerTransport } from './uds-server-transport.js';
|
|
32
35
|
import { TcpServerTransport } from './tcp-server-transport.js';
|
|
33
|
-
import {
|
|
36
|
+
import { resolveContainerSpawnCommand } from './container-command.js';
|
|
37
|
+
import { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequestSchema, ListRootsRequestSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
34
38
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
35
39
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
36
|
-
import { appendFileSync,
|
|
37
|
-
import { atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
|
|
40
|
+
import { appendFileSync, mkdtempSync, writeFileSync, writeSync } from 'node:fs';
|
|
38
41
|
import { join, resolve } from 'node:path';
|
|
39
42
|
import { fileURLToPath } from 'node:url';
|
|
40
43
|
import { tmpdir, homedir } from 'node:os';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import { AuditLog } from './audit-log.js';
|
|
46
|
-
import { prepareToolArgs, rewriteResultContent } from './path-utils.js';
|
|
47
|
-
import { CONTAINER_WORKSPACE_DIR } from '../docker/agent-adapter.js';
|
|
48
|
-
import { extractPolicyRoots, toMcpRoots, directoryForPath } from './policy-roots.js';
|
|
49
|
-
import { checkSandboxAvailability, resolveSandboxConfig, writeServerSettings, wrapServerCommand, cleanupSettingsFiles, annotateSandboxViolation, discoverNodePaths, rewriteServerSettings, } from './sandbox-integration.js';
|
|
50
|
-
import { ROOTS_REFRESH_TIMEOUT_MS } from './mcp-client-manager.js';
|
|
51
|
-
import { CallCircuitBreaker } from './call-circuit-breaker.js';
|
|
52
|
-
import { autoApprove, extractArgsForAutoApprove, readUserContext } from './auto-approver.js';
|
|
53
|
-
import { createLanguageModelFromEnv } from '../config/model-provider.js';
|
|
54
|
-
import { wrapLanguageModel } from 'ai';
|
|
55
|
-
import { createLlmLoggingMiddleware } from '../pipeline/llm-logger.js';
|
|
44
|
+
import { loadGeneratedPolicy, getPackageGeneratedDir } from '../config/index.js';
|
|
45
|
+
import { expandTilde } from '../types/argument-roles.js';
|
|
46
|
+
import { extractPolicyRoots, toMcpRoots } from './policy-roots.js';
|
|
47
|
+
import { checkSandboxAvailability, resolveSandboxConfig, writeServerSettings, wrapServerCommand, cleanupSettingsFiles, discoverNodePaths, rewriteServerSettings, } from './sandbox-integration.js';
|
|
56
48
|
import { extractMcpErrorMessage } from './mcp-error-utils.js';
|
|
57
|
-
import { updateServerContext, formatServerContext } from './server-context.js';
|
|
58
49
|
import { permissiveJsonSchemaValidator } from './permissive-output-validator.js';
|
|
59
50
|
import { VERSION } from '../version.js';
|
|
60
|
-
import { buildTrustedServerSet } from '../memory/memory-annotations.js';
|
|
61
51
|
import { loadToolDescriptionHints, applyToolDescriptionHints } from './tool-description-hints.js';
|
|
62
|
-
import { createApprovalWhitelist, extractWhitelistCandidates, } from './approval-whitelist.js';
|
|
63
52
|
import { getProviderForServer } from '../auth/oauth-registry.js';
|
|
64
53
|
import { loadClientCredentials } from '../auth/oauth-provider.js';
|
|
65
54
|
import { OAuthTokenProvider } from '../auth/oauth-token-provider.js';
|
|
@@ -67,68 +56,10 @@ import { loadOAuthToken } from '../auth/oauth-token-store.js';
|
|
|
67
56
|
import { writeGWorkspaceCredentialFile } from './gworkspace-credentials.js';
|
|
68
57
|
import { TokenFileRefresher } from './token-file-refresher.js';
|
|
69
58
|
import { proxyAnnotations, proxyPolicyRules, proxyToolDefinitions, handleVirtualProxyTool, createControlApiClient, } from '../docker/proxy-tools.js';
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* Module-level state — the proxy is a singleton process.
|
|
75
|
-
*/
|
|
76
|
-
const pendingWhitelistCandidates = new Map();
|
|
77
|
-
/**
|
|
78
|
-
* Adds a root to a client's root list and waits for the server to
|
|
79
|
-
* fetch the updated list. No-op if the root URI is already present.
|
|
80
|
-
* Times out after ROOTS_REFRESH_TIMEOUT_MS if the server never requests
|
|
81
|
-
* the updated list (e.g. servers that don't implement Roots protocol).
|
|
82
|
-
*
|
|
83
|
-
* Returns `'added'` when the server acknowledged the update (called
|
|
84
|
-
* `roots/list`), `'timeout'` when the server didn't respond in time,
|
|
85
|
-
* or `false` when the root was already present.
|
|
86
|
-
*/
|
|
87
|
-
async function addRootToClient(state, root) {
|
|
88
|
-
if (state.roots.some((r) => r.uri === root.uri))
|
|
89
|
-
return false;
|
|
90
|
-
state.roots.push(root);
|
|
91
|
-
let timer;
|
|
92
|
-
let outcome = 'timeout';
|
|
93
|
-
const refreshed = new Promise((resolve) => {
|
|
94
|
-
state.rootsRefreshed = () => {
|
|
95
|
-
clearTimeout(timer);
|
|
96
|
-
outcome = 'added';
|
|
97
|
-
resolve();
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
|
-
const timeout = new Promise((resolve) => {
|
|
101
|
-
timer = setTimeout(() => {
|
|
102
|
-
state.rootsRefreshed = undefined;
|
|
103
|
-
resolve();
|
|
104
|
-
}, ROOTS_REFRESH_TIMEOUT_MS);
|
|
105
|
-
timer.unref();
|
|
106
|
-
});
|
|
107
|
-
await state.client.sendRootsListChanged();
|
|
108
|
-
await Promise.race([refreshed, timeout]);
|
|
109
|
-
return outcome;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Delay before retrying a tool call that returned "access denied" immediately
|
|
113
|
-
* after roots expansion. The filesystem server needs time to async-validate
|
|
114
|
-
* (fs.realpath, fs.stat) the new roots after receiving the rootsListChanged
|
|
115
|
-
* notification. Exported for test access.
|
|
116
|
-
*/
|
|
117
|
-
export const ROOTS_RACE_RETRY_DELAY_MS = 200;
|
|
118
|
-
/**
|
|
119
|
-
* Checks whether an MCP tool call result looks like a roots-race "access denied"
|
|
120
|
-
* error -- the filesystem server rejected the call because it hasn't finished
|
|
121
|
-
* processing the updated roots yet.
|
|
122
|
-
*/
|
|
123
|
-
function isRootsRaceError(result) {
|
|
124
|
-
if (!('isError' in result) || !result.isError)
|
|
125
|
-
return false;
|
|
126
|
-
const text = extractTextFromContent(result.content);
|
|
127
|
-
if (!text)
|
|
128
|
-
return false;
|
|
129
|
-
const lower = text.toLowerCase();
|
|
130
|
-
return lower.includes('access denied') || lower.includes('outside allowed directories');
|
|
131
|
-
}
|
|
59
|
+
import { buildToolMap, assertUniqueToolNames } from './tool-call-pipeline.js';
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Subprocess helpers
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
132
63
|
/** Appends a timestamped line to the session log file. */
|
|
133
64
|
function logToSessionFile(sessionLogPath, message) {
|
|
134
65
|
const timestamp = new Date().toISOString();
|
|
@@ -157,15 +88,6 @@ function getMissingEnvVars(args) {
|
|
|
157
88
|
}
|
|
158
89
|
return missing;
|
|
159
90
|
}
|
|
160
|
-
/** Extracts concatenated text from MCP content blocks. */
|
|
161
|
-
export function extractTextFromContent(content) {
|
|
162
|
-
if (!Array.isArray(content))
|
|
163
|
-
return undefined;
|
|
164
|
-
const texts = content
|
|
165
|
-
.filter((c) => c.type === 'text' && typeof c.text === 'string')
|
|
166
|
-
.map((c) => c.text);
|
|
167
|
-
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
168
|
-
}
|
|
169
91
|
/**
|
|
170
92
|
* Replaces known credential values in a string with `***REDACTED***`.
|
|
171
93
|
* Prevents credential leakage in session log files.
|
|
@@ -179,163 +101,40 @@ function redactCredentials(text, credentials) {
|
|
|
179
101
|
}
|
|
180
102
|
return result;
|
|
181
103
|
}
|
|
182
|
-
const ESCALATION_POLL_INTERVAL_MS = 500;
|
|
183
|
-
const DEFAULT_ESCALATION_TIMEOUT_SECONDS = 300;
|
|
184
|
-
/** Auto-approve trusted input older than this is rejected as stale. */
|
|
185
|
-
const TRUSTED_INPUT_STALENESS_MS = 120_000;
|
|
186
|
-
/**
|
|
187
|
-
* Returns true when a user context is safe to act on for auto-approval.
|
|
188
|
-
*
|
|
189
|
-
* For PTY sessions (isPtySession=true):
|
|
190
|
-
* - source must be 'mux-trusted-input' (set by the trusted mux layer)
|
|
191
|
-
* - timestamp must be present, valid, not in the future, and within
|
|
192
|
-
* TRUSTED_INPUT_STALENESS_MS of `now`
|
|
193
|
-
*
|
|
194
|
-
* For non-PTY sessions:
|
|
195
|
-
* - source is not checked (no mux layer in the path)
|
|
196
|
-
* - a missing or invalid timestamp does not prevent trust; a valid stale
|
|
197
|
-
* timestamp still prevents trust (belt-and-suspenders for future callers)
|
|
198
|
-
*
|
|
199
|
-
* @param context - The user context parsed from user-context.json
|
|
200
|
-
* @param isPtySession - Whether this is a PTY interactive session
|
|
201
|
-
* @param now - Current time in ms; defaults to Date.now() (injectable for tests)
|
|
202
|
-
*/
|
|
203
|
-
export function isUserContextTrusted(context, isPtySession, now = Date.now()) {
|
|
204
|
-
const sourceValid = !isPtySession || context.source === 'mux-trusted-input';
|
|
205
|
-
if (!sourceValid)
|
|
206
|
-
return false;
|
|
207
|
-
let stale = isPtySession; // non-PTY sessions don't require timestamps
|
|
208
|
-
if (context.timestamp !== undefined) {
|
|
209
|
-
const tsMs = new Date(context.timestamp).getTime();
|
|
210
|
-
if (!Number.isNaN(tsMs)) {
|
|
211
|
-
const ageMs = now - tsMs;
|
|
212
|
-
stale = ageMs > TRUSTED_INPUT_STALENESS_MS || ageMs < 0;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return !stale;
|
|
216
|
-
}
|
|
217
|
-
/** Reads escalation timeout from env var, falling back to default. */
|
|
218
|
-
function getEscalationTimeoutMs() {
|
|
219
|
-
const envValue = process.env.ESCALATION_TIMEOUT_SECONDS;
|
|
220
|
-
if (envValue) {
|
|
221
|
-
const parsed = Number(envValue);
|
|
222
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
223
|
-
return parsed * 1000;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return DEFAULT_ESCALATION_TIMEOUT_SECONDS * 1000;
|
|
227
|
-
}
|
|
228
|
-
/** Silently removes a file. Ignores errors (e.g. file already gone). */
|
|
229
|
-
function tryUnlink(path) {
|
|
230
|
-
try {
|
|
231
|
-
unlinkSync(path);
|
|
232
|
-
}
|
|
233
|
-
catch {
|
|
234
|
-
/* ignore */
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
104
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
105
|
+
* True when this proxy subprocess is in a viable serving state -- either
|
|
106
|
+
* virtual-only mode (no backends configured) or at least one configured
|
|
107
|
+
* backend connected. Returning false means the parent should treat this
|
|
108
|
+
* subprocess as failed; otherwise it would register zero tools for the
|
|
109
|
+
* server and trigger a misleading "stale annotations" warning in the
|
|
110
|
+
* coordinator's drift check.
|
|
241
111
|
*/
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
tryUnlink(responsePath);
|
|
112
|
+
export function proxyCanServe(serversConfig, connectedBackendCount) {
|
|
113
|
+
return Object.keys(serversConfig).length === 0 || connectedBackendCount > 0;
|
|
245
114
|
}
|
|
246
115
|
/**
|
|
247
|
-
*
|
|
248
|
-
* Returns the response data, or undefined if the file is not present.
|
|
249
|
-
*/
|
|
250
|
-
function readEscalationResponse(responsePath) {
|
|
251
|
-
if (!existsSync(responsePath))
|
|
252
|
-
return undefined;
|
|
253
|
-
const raw = JSON.parse(readFileSync(responsePath, 'utf-8'));
|
|
254
|
-
// Validate whitelistSelection: must be an integer if present, discard otherwise
|
|
255
|
-
let whitelistSelection;
|
|
256
|
-
if (typeof raw.whitelistSelection === 'number' && Number.isInteger(raw.whitelistSelection)) {
|
|
257
|
-
whitelistSelection = raw.whitelistSelection;
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
decision: raw.decision,
|
|
261
|
-
...(whitelistSelection !== undefined ? { whitelistSelection } : {}),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Waits for a human decision via file-based IPC.
|
|
266
|
-
*
|
|
267
|
-
* Writes a request file to the escalation directory, then polls
|
|
268
|
-
* for a response file. The session process (on the other side)
|
|
269
|
-
* detects the request, surfaces it to the user, and writes the response.
|
|
116
|
+
* Writes a diagnostic line to stderr and (if configured) the proxy session log.
|
|
270
117
|
*
|
|
271
|
-
*
|
|
118
|
+
* Uses `fs.writeSync` so the bytes reach the OS before the function returns
|
|
119
|
+
* even when stderr is a pipe (parent-spawned subprocess). `process.stderr.write`
|
|
120
|
+
* is asynchronous in that case, which would let a follow-up `process.exit(1)`
|
|
121
|
+
* truncate the diagnostic before delivery.
|
|
272
122
|
*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const deadline = Date.now() + getEscalationTimeoutMs();
|
|
278
|
-
while (Date.now() < deadline) {
|
|
279
|
-
const response = readEscalationResponse(responsePath);
|
|
280
|
-
if (response) {
|
|
281
|
-
cleanupEscalationFiles(requestPath, responsePath);
|
|
282
|
-
return response;
|
|
283
|
-
}
|
|
284
|
-
await new Promise((r) => setTimeout(r, ESCALATION_POLL_INTERVAL_MS));
|
|
285
|
-
}
|
|
286
|
-
// Final check -- response may have arrived between last poll and deadline
|
|
287
|
-
const lateResponse = readEscalationResponse(responsePath);
|
|
288
|
-
cleanupEscalationFiles(requestPath, responsePath);
|
|
289
|
-
return lateResponse ?? { decision: 'denied' };
|
|
123
|
+
function emitProxyDiagnostic(level, message, sessionLogPath) {
|
|
124
|
+
writeSync(2, `${level}: ${message}\n`);
|
|
125
|
+
if (sessionLogPath)
|
|
126
|
+
logToSessionFile(sessionLogPath, `[proxy] ${message}`);
|
|
290
127
|
}
|
|
291
|
-
/**
|
|
292
|
-
* Creates the auto-approve LLM model from environment variables.
|
|
293
|
-
* Returns null when auto-approve is not enabled or env vars are missing.
|
|
294
|
-
* Wraps with LLM logging middleware when a log path is provided.
|
|
295
|
-
*/
|
|
296
|
-
async function createAutoApproveModel(sessionLogPath) {
|
|
297
|
-
if (process.env.AUTO_APPROVE_ENABLED !== 'true')
|
|
298
|
-
return null;
|
|
299
|
-
const modelId = process.env.AUTO_APPROVE_MODEL_ID;
|
|
300
|
-
if (!modelId)
|
|
301
|
-
return null;
|
|
302
|
-
const apiKey = process.env.AUTO_APPROVE_API_KEY ?? '';
|
|
303
|
-
try {
|
|
304
|
-
const baseModel = await createLanguageModelFromEnv(modelId, apiKey);
|
|
305
|
-
const llmLogPath = process.env.AUTO_APPROVE_LLM_LOG_PATH;
|
|
306
|
-
if (sessionLogPath) {
|
|
307
|
-
logToSessionFile(sessionLogPath, `Auto-approve model created: ${modelId}`);
|
|
308
|
-
}
|
|
309
|
-
if (!llmLogPath)
|
|
310
|
-
return baseModel;
|
|
311
|
-
return wrapLanguageModel({
|
|
312
|
-
model: baseModel,
|
|
313
|
-
middleware: createLlmLoggingMiddleware(llmLogPath, { stepName: 'auto-approve' }),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
// Model creation failure should not prevent the proxy from starting.
|
|
318
|
-
// Auto-approve simply won't be available for this session.
|
|
319
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
320
|
-
if (sessionLogPath) {
|
|
321
|
-
logToSessionFile(sessionLogPath, `Auto-approve model creation failed for ${modelId}: ${message}`);
|
|
322
|
-
}
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
// ── Extracted functions ────────────────────────────────────────────────
|
|
327
128
|
/**
|
|
328
129
|
* Reads and validates proxy environment variables.
|
|
329
130
|
* Returns a typed config object or calls process.exit(1) on missing required vars.
|
|
330
131
|
*/
|
|
331
132
|
export function parseProxyEnvConfig() {
|
|
332
|
-
const auditLogPath = process.env.AUDIT_LOG_PATH ?? './audit.jsonl';
|
|
333
133
|
const serversConfigJson = process.env.MCP_SERVERS_CONFIG;
|
|
334
134
|
const generatedDir = process.env.GENERATED_DIR;
|
|
335
135
|
const protectedPathsJson = process.env.PROTECTED_PATHS ?? '[]';
|
|
336
136
|
const sessionLogPath = process.env.SESSION_LOG_PATH;
|
|
337
137
|
const allowedDirectory = process.env.ALLOWED_DIRECTORY;
|
|
338
|
-
const escalationDir = process.env.ESCALATION_DIR;
|
|
339
138
|
if (!serversConfigJson) {
|
|
340
139
|
process.stderr.write('MCP_SERVERS_CONFIG environment variable is required\n');
|
|
341
140
|
process.exit(1);
|
|
@@ -351,7 +150,6 @@ export function parseProxyEnvConfig() {
|
|
|
351
150
|
: {};
|
|
352
151
|
delete process.env.SERVER_CREDENTIALS;
|
|
353
152
|
const sandboxPolicy = (process.env.SANDBOX_POLICY ?? 'warn');
|
|
354
|
-
const auditRedaction = process.env.AUDIT_REDACTION === 'true';
|
|
355
153
|
const allServersConfig = JSON.parse(serversConfigJson);
|
|
356
154
|
const protectedPaths = JSON.parse(protectedPathsJson);
|
|
357
155
|
// When SERVER_FILTER is set, only connect to that single backend server.
|
|
@@ -373,17 +171,15 @@ export function parseProxyEnvConfig() {
|
|
|
373
171
|
// Otherwise they default to the same directory as compiled policy.
|
|
374
172
|
const toolAnnotationsDir = process.env.TOOL_ANNOTATIONS_DIR ?? generatedDir;
|
|
375
173
|
return {
|
|
376
|
-
auditLogPath,
|
|
377
174
|
serversConfig,
|
|
378
175
|
generatedDir,
|
|
379
176
|
toolAnnotationsDir,
|
|
380
177
|
protectedPaths,
|
|
381
178
|
sessionLogPath,
|
|
382
179
|
allowedDirectory,
|
|
383
|
-
escalationDir,
|
|
384
180
|
serverCredentials,
|
|
385
181
|
sandboxPolicy,
|
|
386
|
-
|
|
182
|
+
serverFilter,
|
|
387
183
|
};
|
|
388
184
|
}
|
|
389
185
|
/**
|
|
@@ -433,365 +229,6 @@ export function resolveServerSandboxConfigs(serversConfig, allowedDirectory, san
|
|
|
433
229
|
}
|
|
434
230
|
return { resolvedSandboxConfigs, settingsDir, serverCwdPaths };
|
|
435
231
|
}
|
|
436
|
-
/**
|
|
437
|
-
* Validates that all argument keys exist in the tool's inputSchema.
|
|
438
|
-
* Returns null if valid, or an error message listing unknown and valid keys.
|
|
439
|
-
* Skips validation when the schema has no properties or allows additional properties.
|
|
440
|
-
*/
|
|
441
|
-
export function validateToolArguments(args, inputSchema) {
|
|
442
|
-
const properties = inputSchema.properties;
|
|
443
|
-
if (!properties || typeof properties !== 'object')
|
|
444
|
-
return null;
|
|
445
|
-
if (inputSchema.additionalProperties)
|
|
446
|
-
return null;
|
|
447
|
-
const validKeys = new Set(Object.keys(properties));
|
|
448
|
-
const unknownKeys = Object.keys(args).filter((k) => !validKeys.has(k));
|
|
449
|
-
if (unknownKeys.length === 0)
|
|
450
|
-
return null;
|
|
451
|
-
const unknownList = unknownKeys.map((k) => `"${k}"`).join(', ');
|
|
452
|
-
const validList = [...validKeys]
|
|
453
|
-
.sort()
|
|
454
|
-
.map((k) => `"${k}"`)
|
|
455
|
-
.join(', ');
|
|
456
|
-
return `Unknown argument(s): ${unknownList}. Valid parameters are: ${validList}`;
|
|
457
|
-
}
|
|
458
|
-
/** Builds a lookup map from tool name to ProxiedTool for routing. */
|
|
459
|
-
export function buildToolMap(allTools) {
|
|
460
|
-
const toolMap = new Map();
|
|
461
|
-
for (const tool of allTools) {
|
|
462
|
-
toolMap.set(tool.name, tool);
|
|
463
|
-
}
|
|
464
|
-
return toolMap;
|
|
465
|
-
}
|
|
466
|
-
/**
|
|
467
|
-
* Creates an audit entry and logs it. Extracted so the audit shape
|
|
468
|
-
* construction is testable independently.
|
|
469
|
-
*/
|
|
470
|
-
export function buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, options) {
|
|
471
|
-
return {
|
|
472
|
-
timestamp: request.timestamp,
|
|
473
|
-
requestId: request.requestId,
|
|
474
|
-
serverName: request.serverName,
|
|
475
|
-
toolName: request.toolName,
|
|
476
|
-
arguments: argsForTransport,
|
|
477
|
-
policyDecision,
|
|
478
|
-
escalationResult: options.escalationResult,
|
|
479
|
-
result,
|
|
480
|
-
durationMs,
|
|
481
|
-
sandboxed: options.sandboxed || undefined,
|
|
482
|
-
autoApproved: options.autoApproved || undefined,
|
|
483
|
-
whitelistApproved: options.whitelistApproved || undefined,
|
|
484
|
-
whitelistPatternId: options.whitelistPatternId,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
/**
|
|
488
|
-
* Handles a single tool call request: policy evaluation, escalation,
|
|
489
|
-
* circuit breaker check, and forwarding to the real MCP server.
|
|
490
|
-
*
|
|
491
|
-
* This is the core security logic extracted from the CallTool handler
|
|
492
|
-
* for independent unit testing.
|
|
493
|
-
*/
|
|
494
|
-
export async function handleCallTool(toolName, rawArgs, deps) {
|
|
495
|
-
const toolInfo = deps.toolMap.get(toolName);
|
|
496
|
-
if (!toolInfo) {
|
|
497
|
-
return {
|
|
498
|
-
content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
|
|
499
|
-
isError: true,
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
// Annotation-driven normalization: split into transport vs policy args.
|
|
503
|
-
// Trusted servers skip annotation lookup and prepareToolArgs — use raw args directly.
|
|
504
|
-
const annotation = deps.policyEngine.getAnnotation(toolInfo.serverName, toolInfo.name, rawArgs);
|
|
505
|
-
const isTrusted = !annotation && deps.policyEngine.isTrustedServer(toolInfo.serverName);
|
|
506
|
-
if (!annotation && !isTrusted) {
|
|
507
|
-
return {
|
|
508
|
-
content: [
|
|
509
|
-
{
|
|
510
|
-
type: 'text',
|
|
511
|
-
text: `Missing annotation for tool: ${toolInfo.serverName}__${toolInfo.name}. Re-run 'ironcurtain annotate-tools' to update.`,
|
|
512
|
-
},
|
|
513
|
-
],
|
|
514
|
-
isError: true,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
// Validate argument names against the tool's schema.
|
|
518
|
-
// Skip for trusted servers (check directly, not via isTrusted which also requires missing annotation).
|
|
519
|
-
if (!deps.policyEngine.isTrustedServer(toolInfo.serverName)) {
|
|
520
|
-
const validationError = validateToolArguments(rawArgs, toolInfo.inputSchema);
|
|
521
|
-
if (validationError) {
|
|
522
|
-
deps.auditLog.log({
|
|
523
|
-
timestamp: new Date().toISOString(),
|
|
524
|
-
requestId: uuidv4(),
|
|
525
|
-
serverName: toolInfo.serverName,
|
|
526
|
-
toolName: toolInfo.name,
|
|
527
|
-
arguments: rawArgs,
|
|
528
|
-
policyDecision: { status: 'deny', rule: 'invalid-arguments', reason: validationError },
|
|
529
|
-
result: { status: 'denied', error: validationError },
|
|
530
|
-
durationMs: 0,
|
|
531
|
-
});
|
|
532
|
-
return {
|
|
533
|
-
content: [{ type: 'text', text: validationError }],
|
|
534
|
-
isError: true,
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
let argsForTransport;
|
|
539
|
-
let argsForPolicy;
|
|
540
|
-
if (isTrusted) {
|
|
541
|
-
// Trusted server with no annotation: use raw args for both transport and policy
|
|
542
|
-
argsForTransport = rawArgs;
|
|
543
|
-
argsForPolicy = rawArgs;
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
// annotation is guaranteed non-null here: we returned early if both are falsy
|
|
547
|
-
const resolvedAnnotation = annotation;
|
|
548
|
-
// Enrich git tool args with the locally tracked working directory when
|
|
549
|
-
// the agent omits the `path` argument. The git MCP server tracks its own
|
|
550
|
-
// CWD (set via git_set_working_dir) and uses it implicitly, but the
|
|
551
|
-
// policy engine needs the explicit path to resolve sandbox containment
|
|
552
|
-
// and the default remote URL for domain-based policy rules.
|
|
553
|
-
// The serverContextMap mirrors the git server's working directory from
|
|
554
|
-
// successful git_set_working_dir / git_clone calls, avoiding an RPC.
|
|
555
|
-
let effectiveRawArgs = rawArgs;
|
|
556
|
-
const hasEffectivePath = 'path' in rawArgs && typeof rawArgs.path === 'string' && rawArgs.path.trim() !== '';
|
|
557
|
-
if (toolInfo.serverName === 'git' && 'path' in resolvedAnnotation.args && !hasEffectivePath) {
|
|
558
|
-
const gitWorkDir = deps.serverContextMap.get('git')?.workingDirectory;
|
|
559
|
-
if (!gitWorkDir) {
|
|
560
|
-
const errorMsg = 'Git server has no working directory set. Call git_set_working_dir first.';
|
|
561
|
-
deps.auditLog.log(buildAuditEntry({
|
|
562
|
-
requestId: uuidv4(),
|
|
563
|
-
serverName: toolInfo.serverName,
|
|
564
|
-
toolName: toolInfo.name,
|
|
565
|
-
arguments: rawArgs,
|
|
566
|
-
timestamp: new Date().toISOString(),
|
|
567
|
-
}, rawArgs, { status: 'deny', rule: 'git-path-enrichment-failed', reason: errorMsg }, { status: 'denied', error: errorMsg }, 0, {}));
|
|
568
|
-
return {
|
|
569
|
-
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
|
|
570
|
-
isError: true,
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
effectiveRawArgs = { ...rawArgs, path: gitWorkDir };
|
|
574
|
-
}
|
|
575
|
-
({ argsForTransport, argsForPolicy } = prepareToolArgs(effectiveRawArgs, resolvedAnnotation, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR));
|
|
576
|
-
}
|
|
577
|
-
const request = {
|
|
578
|
-
requestId: uuidv4(),
|
|
579
|
-
serverName: toolInfo.serverName,
|
|
580
|
-
toolName: toolInfo.name,
|
|
581
|
-
arguments: argsForPolicy,
|
|
582
|
-
timestamp: new Date().toISOString(),
|
|
583
|
-
};
|
|
584
|
-
const evaluation = deps.policyEngine.evaluate(request);
|
|
585
|
-
const policyDecision = {
|
|
586
|
-
status: evaluation.decision,
|
|
587
|
-
rule: evaluation.rule,
|
|
588
|
-
reason: evaluation.reason,
|
|
589
|
-
};
|
|
590
|
-
let escalationResult;
|
|
591
|
-
let autoApproved = false;
|
|
592
|
-
let whitelistApproved = false;
|
|
593
|
-
let whitelistPatternId;
|
|
594
|
-
let rootsExpanded = false;
|
|
595
|
-
const serverSandboxConfig = deps.resolvedSandboxConfigs.get(toolInfo.serverName);
|
|
596
|
-
const serverIsSandboxed = serverSandboxConfig?.sandboxed === true;
|
|
597
|
-
function logAudit(result, durationMs, overrideEscalation) {
|
|
598
|
-
const entry = buildAuditEntry(request, argsForTransport, policyDecision, result, durationMs, {
|
|
599
|
-
escalationResult: overrideEscalation ?? escalationResult,
|
|
600
|
-
sandboxed: serverIsSandboxed,
|
|
601
|
-
autoApproved,
|
|
602
|
-
whitelistApproved: whitelistApproved || undefined,
|
|
603
|
-
whitelistPatternId,
|
|
604
|
-
});
|
|
605
|
-
deps.auditLog.log(entry);
|
|
606
|
-
}
|
|
607
|
-
if (evaluation.decision === 'escalate') {
|
|
608
|
-
// Annotation is guaranteed non-null here: trusted servers always allow (never escalate),
|
|
609
|
-
// and missing-annotation returns early above. Narrow the type for TypeScript.
|
|
610
|
-
const resolvedAnnotation = annotation;
|
|
611
|
-
const whitelistMatch = deps.whitelist.match(toolInfo.serverName, toolInfo.name, argsForPolicy, resolvedAnnotation);
|
|
612
|
-
if (whitelistMatch.matched) {
|
|
613
|
-
whitelistApproved = true;
|
|
614
|
-
whitelistPatternId = whitelistMatch.patternId;
|
|
615
|
-
escalationResult = 'approved';
|
|
616
|
-
policyDecision.status = 'allow';
|
|
617
|
-
policyDecision.reason = `Whitelist-approved: ${whitelistMatch.pattern.description}`;
|
|
618
|
-
}
|
|
619
|
-
if (!whitelistApproved) {
|
|
620
|
-
if (!deps.escalationDir) {
|
|
621
|
-
logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
|
|
622
|
-
return {
|
|
623
|
-
content: [
|
|
624
|
-
{
|
|
625
|
-
type: 'text',
|
|
626
|
-
text: `ESCALATION REQUIRED: ${evaluation.reason}. Action denied (no escalation handler).`,
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
isError: true,
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
const escalationId = uuidv4();
|
|
633
|
-
// Try auto-approve before falling through to human escalation
|
|
634
|
-
if (deps.autoApproveModel) {
|
|
635
|
-
const userContext = readUserContext(deps.escalationDir);
|
|
636
|
-
if (userContext) {
|
|
637
|
-
const isPtySession = process.env.IRONCURTAIN_PTY_SESSION === '1';
|
|
638
|
-
if (isUserContextTrusted(userContext, isPtySession)) {
|
|
639
|
-
const autoResult = await autoApprove({
|
|
640
|
-
userMessage: userContext.userMessage,
|
|
641
|
-
toolName: `${toolInfo.serverName}/${toolInfo.name}`,
|
|
642
|
-
escalationReason: evaluation.reason,
|
|
643
|
-
arguments: extractArgsForAutoApprove(argsForPolicy, annotation),
|
|
644
|
-
}, deps.autoApproveModel);
|
|
645
|
-
if (autoResult.decision === 'approve') {
|
|
646
|
-
autoApproved = true;
|
|
647
|
-
escalationResult = 'approved';
|
|
648
|
-
policyDecision.status = 'allow';
|
|
649
|
-
policyDecision.reason = `Auto-approved: ${autoResult.reasoning}`;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
if (!autoApproved) {
|
|
655
|
-
// Extract whitelist candidates just before human escalation (avoids
|
|
656
|
-
// wasted work when auto-approve succeeds).
|
|
657
|
-
const { patterns: candidatePatterns, ipcs: candidateIpcs } = extractWhitelistCandidates(toolInfo.serverName, toolInfo.name, argsForPolicy, resolvedAnnotation, evaluation.escalatedRoles, escalationId, evaluation.reason);
|
|
658
|
-
if (candidatePatterns.length > 0) {
|
|
659
|
-
// Store full patterns in proxy memory, keyed by escalation ID
|
|
660
|
-
pendingWhitelistCandidates.set(escalationId, candidatePatterns);
|
|
661
|
-
}
|
|
662
|
-
try {
|
|
663
|
-
const response = await waitForEscalationDecision(deps.escalationDir, {
|
|
664
|
-
escalationId,
|
|
665
|
-
serverName: request.serverName,
|
|
666
|
-
toolName: request.toolName,
|
|
667
|
-
arguments: argsForTransport,
|
|
668
|
-
reason: evaluation.reason,
|
|
669
|
-
context: formatServerContext(deps.serverContextMap, toolInfo.serverName),
|
|
670
|
-
whitelistCandidates: candidateIpcs.length > 0 ? candidateIpcs : undefined,
|
|
671
|
-
});
|
|
672
|
-
if (response.decision === 'denied') {
|
|
673
|
-
logAudit({ status: 'denied', error: evaluation.reason }, 0, 'denied');
|
|
674
|
-
return {
|
|
675
|
-
content: [{ type: 'text', text: `ESCALATION DENIED: ${evaluation.reason}` }],
|
|
676
|
-
isError: true,
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
escalationResult = 'approved';
|
|
680
|
-
policyDecision.status = 'allow';
|
|
681
|
-
policyDecision.reason = 'Approved by human during escalation';
|
|
682
|
-
// Handle whitelist selection from the response
|
|
683
|
-
if (response.whitelistSelection !== undefined) {
|
|
684
|
-
const storedPatterns = pendingWhitelistCandidates.get(escalationId);
|
|
685
|
-
if (storedPatterns &&
|
|
686
|
-
response.whitelistSelection >= 0 &&
|
|
687
|
-
response.whitelistSelection < storedPatterns.length) {
|
|
688
|
-
const selectedPattern = storedPatterns[response.whitelistSelection];
|
|
689
|
-
deps.whitelist.add(selectedPattern);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
finally {
|
|
694
|
-
// Issue 12: ensure cleanup even if waitForEscalationDecision throws
|
|
695
|
-
pendingWhitelistCandidates.delete(escalationId);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
// Expand roots for approved path arguments only (skip URLs, opaques)
|
|
700
|
-
const state = deps.clientStates.get(toolInfo.serverName);
|
|
701
|
-
if (state && annotation) {
|
|
702
|
-
const pathValues = extractAnnotatedPaths(argsForTransport, annotation, getPathRoles());
|
|
703
|
-
for (const p of pathValues) {
|
|
704
|
-
const result = await addRootToClient(state, {
|
|
705
|
-
uri: `file://${directoryForPath(p)}`,
|
|
706
|
-
name: 'escalation-approved',
|
|
707
|
-
});
|
|
708
|
-
if (result === 'added')
|
|
709
|
-
rootsExpanded = true;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (evaluation.decision === 'deny') {
|
|
714
|
-
logAudit({ status: 'denied', error: evaluation.reason }, 0);
|
|
715
|
-
return {
|
|
716
|
-
content: [{ type: 'text', text: `DENIED: ${evaluation.reason}` }],
|
|
717
|
-
isError: true,
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
// Circuit breaker: deny if the same tool+args is called too many times
|
|
721
|
-
const cbVerdict = deps.circuitBreaker.check(toolInfo.name, argsForTransport);
|
|
722
|
-
if (!cbVerdict.allowed) {
|
|
723
|
-
logAudit({ status: 'denied', error: cbVerdict.reason }, 0);
|
|
724
|
-
return {
|
|
725
|
-
content: [{ type: 'text', text: cbVerdict.reason }],
|
|
726
|
-
isError: true,
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
// Virtual proxy tools: handle locally, no backend forwarding
|
|
730
|
-
if (toolInfo.serverName === 'proxy' && deps.controlApiClient) {
|
|
731
|
-
const startTime = Date.now();
|
|
732
|
-
try {
|
|
733
|
-
const result = await handleVirtualProxyTool(toolInfo.name, argsForTransport, deps.controlApiClient);
|
|
734
|
-
logAudit({ status: 'success' }, Date.now() - startTime);
|
|
735
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
736
|
-
}
|
|
737
|
-
catch (err) {
|
|
738
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
739
|
-
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
740
|
-
return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
// Policy allows -- forward to the real MCP server with transport args
|
|
744
|
-
const startTime = Date.now();
|
|
745
|
-
try {
|
|
746
|
-
const clientState = deps.clientStates.get(toolInfo.serverName);
|
|
747
|
-
if (!clientState) {
|
|
748
|
-
const err = `Internal error: no client connection for server "${toolInfo.serverName}"`;
|
|
749
|
-
logAudit({ status: 'denied', error: err }, 0);
|
|
750
|
-
return { content: [{ type: 'text', text: err }], isError: true };
|
|
751
|
-
}
|
|
752
|
-
const client = clientState.client;
|
|
753
|
-
// CompatibilityCallToolResultSchema accepts the legacy `toolResult` response format
|
|
754
|
-
// (protocol version 2024-10-07). Output schema validation is intentionally
|
|
755
|
-
// bypassed by the permissiveJsonSchemaValidator injected at Client construction time.
|
|
756
|
-
const callToolArgs = { name: toolInfo.name, arguments: argsForTransport };
|
|
757
|
-
const callToolOpts = { timeout: getEscalationTimeoutMs() };
|
|
758
|
-
let result = await client.callTool(callToolArgs, CompatibilityCallToolResultSchema, callToolOpts);
|
|
759
|
-
// Race condition mitigation: after roots expansion the filesystem server
|
|
760
|
-
// has been notified but may not have finished async-validating the new
|
|
761
|
-
// roots (fs.realpath, fs.stat). Retry once after a short delay.
|
|
762
|
-
if (rootsExpanded && isRootsRaceError(result)) {
|
|
763
|
-
const initialError = extractTextFromContent(result.content) ?? 'access denied (roots race)';
|
|
764
|
-
logAudit({ status: 'error', error: `Roots race detected, retrying: ${initialError}` }, Date.now() - startTime);
|
|
765
|
-
await new Promise((r) => setTimeout(r, ROOTS_RACE_RETRY_DELAY_MS));
|
|
766
|
-
result = await client.callTool(callToolArgs, CompatibilityCallToolResultSchema, callToolOpts);
|
|
767
|
-
}
|
|
768
|
-
// Reverse-rewrite host sandbox paths to container workspace paths in results
|
|
769
|
-
const rewrittenContent = deps.allowedDirectory
|
|
770
|
-
? rewriteResultContent(result.content, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR)
|
|
771
|
-
: result.content;
|
|
772
|
-
if (result.isError) {
|
|
773
|
-
const errorText = extractTextFromContent(rewrittenContent) ?? 'Unknown tool error';
|
|
774
|
-
const errorMessage = annotateSandboxViolation(errorText, serverIsSandboxed);
|
|
775
|
-
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
776
|
-
return { content: rewrittenContent, isError: true };
|
|
777
|
-
}
|
|
778
|
-
updateServerContext(deps.serverContextMap, toolInfo.serverName, toolInfo.name, argsForTransport);
|
|
779
|
-
logAudit({ status: 'success' }, Date.now() - startTime);
|
|
780
|
-
return { content: rewrittenContent };
|
|
781
|
-
}
|
|
782
|
-
catch (err) {
|
|
783
|
-
const rawError = extractMcpErrorMessage(err);
|
|
784
|
-
const errorMessage = annotateSandboxViolation(rawError, serverIsSandboxed);
|
|
785
|
-
logAudit({ status: 'error', error: errorMessage }, Date.now() - startTime);
|
|
786
|
-
const errorContent = [{ type: 'text', text: `Error: ${errorMessage}` }];
|
|
787
|
-
return {
|
|
788
|
-
content: deps.allowedDirectory
|
|
789
|
-
? rewriteResultContent(errorContent, deps.allowedDirectory, CONTAINER_WORKSPACE_DIR)
|
|
790
|
-
: errorContent,
|
|
791
|
-
isError: true,
|
|
792
|
-
};
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
232
|
/**
|
|
796
233
|
* Selects and validates the proxy transport based on environment variables.
|
|
797
234
|
* Returns 'tcp', 'uds', or 'stdio' along with the transport options.
|
|
@@ -814,27 +251,30 @@ export function selectTransportConfig() {
|
|
|
814
251
|
// ── main() -- thin orchestrator ────────────────────────────────────────
|
|
815
252
|
async function main() {
|
|
816
253
|
const envConfig = parseProxyEnvConfig();
|
|
817
|
-
const {
|
|
818
|
-
|
|
254
|
+
const { serversConfig, generatedDir, toolAnnotationsDir, sessionLogPath, allowedDirectory, serverCredentials, sandboxPolicy, serverFilter, } = envConfig;
|
|
255
|
+
// Load compiled policy + annotations to derive MCP roots advertised
|
|
256
|
+
// to backend MCP servers at connection time (the backend still uses
|
|
257
|
+
// Roots to authorize filesystem access).
|
|
258
|
+
const { compiledPolicy, toolAnnotations } = loadGeneratedPolicy({
|
|
819
259
|
policyDir: generatedDir,
|
|
820
260
|
toolAnnotationsDir,
|
|
821
261
|
fallbackDir: getPackageGeneratedDir(),
|
|
822
262
|
});
|
|
823
|
-
// Merge proxy tool annotations and policy rules
|
|
824
|
-
//
|
|
263
|
+
// Merge proxy tool annotations and policy rules so roots derivation
|
|
264
|
+
// includes any paths referenced by proxy policy rules.
|
|
825
265
|
toolAnnotations.servers.proxy = {
|
|
826
266
|
inputHash: 'hardcoded',
|
|
827
267
|
tools: proxyAnnotations,
|
|
828
268
|
};
|
|
829
269
|
compiledPolicy.rules = [...proxyPolicyRules, ...compiledPolicy.rules];
|
|
830
|
-
const serverDomainAllowlists = extractServerDomainAllowlists(serversConfig);
|
|
831
|
-
const trustedServers = buildTrustedServerSet(serversConfig);
|
|
832
|
-
const policyEngine = new PolicyEngine(compiledPolicy, toolAnnotations, protectedPaths, allowedDirectory, serverDomainAllowlists, dynamicLists, trustedServers);
|
|
833
|
-
const auditLog = new AuditLog(auditLogPath, { redact: auditRedaction });
|
|
834
|
-
const circuitBreaker = new CallCircuitBreaker();
|
|
835
|
-
const autoApproveModel = await createAutoApproveModel(sessionLogPath);
|
|
836
270
|
const policyRoots = extractPolicyRoots(compiledPolicy, allowedDirectory ?? '/tmp');
|
|
837
271
|
const mcpRoots = toMcpRoots(policyRoots);
|
|
272
|
+
// Shared mutable roots array bridged to the coordinator. The proxy
|
|
273
|
+
// server's `notifications/roots/list_changed` handler (registered
|
|
274
|
+
// below) refreshes this in place via `server.listRoots()`, and each
|
|
275
|
+
// backend client's `roots/list` handler returns it. All backends
|
|
276
|
+
// observe the same authoritative set.
|
|
277
|
+
const relayRoots = [...mcpRoots];
|
|
838
278
|
// ── Sandbox availability & config resolution ──────────────────────
|
|
839
279
|
const { sandboxAvailable } = validateSandboxAvailability(sandboxPolicy, sessionLogPath, process.platform);
|
|
840
280
|
const { resolvedSandboxConfigs, settingsDir, serverCwdPaths } = resolveServerSandboxConfigs(serversConfig, allowedDirectory, sandboxAvailable, sandboxPolicy);
|
|
@@ -849,10 +289,7 @@ async function main() {
|
|
|
849
289
|
// that aren't set — the server will fail to start without them.
|
|
850
290
|
const missingEnvVars = getMissingEnvVars(config.args);
|
|
851
291
|
if (missingEnvVars.length > 0) {
|
|
852
|
-
|
|
853
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
854
|
-
if (sessionLogPath)
|
|
855
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
292
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": missing environment variable(s) ${missingEnvVars.join(', ')}`, sessionLogPath);
|
|
856
293
|
continue;
|
|
857
294
|
}
|
|
858
295
|
// ── OAuth token injection for servers backed by OAuth providers ──
|
|
@@ -861,18 +298,12 @@ async function main() {
|
|
|
861
298
|
if (oauthProvider) {
|
|
862
299
|
const clientCreds = loadClientCredentials(oauthProvider);
|
|
863
300
|
if (!clientCreds) {
|
|
864
|
-
|
|
865
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
866
|
-
if (sessionLogPath)
|
|
867
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
301
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": no OAuth credentials. Run 'ironcurtain auth import ${oauthProvider.id} <file>'`, sessionLogPath);
|
|
868
302
|
continue;
|
|
869
303
|
}
|
|
870
304
|
const tokenProvider = new OAuthTokenProvider(oauthProvider, clientCreds);
|
|
871
305
|
if (!tokenProvider.isAuthorized()) {
|
|
872
|
-
|
|
873
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
874
|
-
if (sessionLogPath)
|
|
875
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
306
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": not authorized. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
|
|
876
307
|
continue;
|
|
877
308
|
}
|
|
878
309
|
try {
|
|
@@ -928,10 +359,7 @@ async function main() {
|
|
|
928
359
|
}
|
|
929
360
|
catch (err) {
|
|
930
361
|
const message = err instanceof Error ? err.message : String(err);
|
|
931
|
-
|
|
932
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
933
|
-
if (sessionLogPath)
|
|
934
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
362
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": OAuth token error: ${message}. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
|
|
935
363
|
continue;
|
|
936
364
|
}
|
|
937
365
|
}
|
|
@@ -946,7 +374,13 @@ async function main() {
|
|
|
946
374
|
rewriteServerSettings(settingsPath, { allowRead: dynamicNodePaths });
|
|
947
375
|
}
|
|
948
376
|
}
|
|
949
|
-
|
|
377
|
+
// Docker-based servers (e.g. github) run under the Apple `container`
|
|
378
|
+
// CLI on machines that have that runtime instead of Docker.
|
|
379
|
+
const spawnSpec = resolveContainerSpawnCommand(config.command, config.args);
|
|
380
|
+
if (spawnSpec.translated) {
|
|
381
|
+
emitProxyDiagnostic('WARNING', `MCP server "${serverName}": docker not found; running via the Apple container CLI instead`, sessionLogPath);
|
|
382
|
+
}
|
|
383
|
+
const wrapped = wrapServerCommand(serverName, spawnSpec.command, spawnSpec.args, resolved, settingsDir);
|
|
950
384
|
const transport = new StdioClientTransport({
|
|
951
385
|
command: wrapped.command,
|
|
952
386
|
args: wrapped.args,
|
|
@@ -983,7 +417,11 @@ async function main() {
|
|
|
983
417
|
capabilities: { roots: { listChanged: true } },
|
|
984
418
|
jsonSchemaValidator: permissiveJsonSchemaValidator,
|
|
985
419
|
});
|
|
986
|
-
|
|
420
|
+
// All backend clients share the single `relayRoots` array so a
|
|
421
|
+
// coordinator-driven roots update (see the proxy server's
|
|
422
|
+
// `roots/list_changed` handler below) is visible to every backend
|
|
423
|
+
// on its next `roots/list` request.
|
|
424
|
+
const state = { client, roots: relayRoots };
|
|
987
425
|
client.setRequestHandler(ListRootsRequestSchema, () => {
|
|
988
426
|
if (state.rootsRefreshed) {
|
|
989
427
|
state.rootsRefreshed();
|
|
@@ -995,9 +433,34 @@ async function main() {
|
|
|
995
433
|
await client.connect(transport);
|
|
996
434
|
}
|
|
997
435
|
catch (err) {
|
|
436
|
+
// Per-server graceful degradation: an unconnectable backend (binary
|
|
437
|
+
// missing, container runtime down, server crash on startup) is
|
|
438
|
+
// skipped with a diagnostic, like the missing-env-var and OAuth
|
|
439
|
+
// paths above. When this leaves the relay with zero backends, the
|
|
440
|
+
// proxyCanServe guard below exits non-zero and the parent skips
|
|
441
|
+
// the relay — a startup failure never silently widens or narrows
|
|
442
|
+
// policy, since unregistered tools are denied by default.
|
|
998
443
|
const cmd = `${wrapped.command} ${wrapped.args.join(' ')}`;
|
|
999
|
-
|
|
1000
|
-
|
|
444
|
+
// Redact credentials/OAuth env from the captured stderr before it
|
|
445
|
+
// reaches emitProxyDiagnostic (which persists to the session log),
|
|
446
|
+
// matching the redaction the streaming stderr handler applies above.
|
|
447
|
+
const redactedStderr = redactCredentials(redactCredentials(serverStderr, serverCredentials), oauthEnv);
|
|
448
|
+
const stderrSnippet = redactedStderr ? ` Server stderr: ${redactedStderr.substring(0, 1000)}` : '';
|
|
449
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": failed to connect (${cmd}): ${err instanceof Error ? err.message : String(err)}${stderrSnippet}`, sessionLogPath);
|
|
450
|
+
// `client.connect` may have spawned the backend subprocess (via
|
|
451
|
+
// StdioClientTransport) before failing; close it so a half-started
|
|
452
|
+
// backend doesn't leak a child process for the relay's lifetime.
|
|
453
|
+
await client.close().catch(() => { });
|
|
454
|
+
// This server is being skipped; stop the OAuth refresher started for it
|
|
455
|
+
// above so it does not keep running force-refresh loops for a backend
|
|
456
|
+
// that never connected (shutdown-time cleanup would otherwise be the
|
|
457
|
+
// only thing that stops it).
|
|
458
|
+
const orphanedRefresher = tokenRefreshers.get(serverName);
|
|
459
|
+
if (orphanedRefresher) {
|
|
460
|
+
orphanedRefresher.stop();
|
|
461
|
+
tokenRefreshers.delete(serverName);
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
1001
464
|
}
|
|
1002
465
|
clientStates.set(serverName, state);
|
|
1003
466
|
const result = await client.listTools();
|
|
@@ -1010,9 +473,21 @@ async function main() {
|
|
|
1010
473
|
});
|
|
1011
474
|
}
|
|
1012
475
|
}
|
|
476
|
+
// Exit non-zero so the parent's `MCPClientManager.connect()` fails the
|
|
477
|
+
// handshake -- the existing try/catch in `connectBackendSubprocesses`
|
|
478
|
+
// skips the server without reaching `registerTools`.
|
|
479
|
+
if (!proxyCanServe(serversConfig, clientStates.size)) {
|
|
480
|
+
const serverNames = Object.keys(serversConfig).join(', ');
|
|
481
|
+
emitProxyDiagnostic('ERROR', `proxy subprocess for SERVER_FILTER="${serverFilter ?? '<unset>'}" has no connected backend (configured: ${serverNames}); exiting`, sessionLogPath);
|
|
482
|
+
for (const refresher of tokenRefreshers.values()) {
|
|
483
|
+
refresher.stop();
|
|
484
|
+
}
|
|
485
|
+
cleanupSettingsFiles(settingsDir);
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
1013
488
|
// In virtual-only mode (SERVER_FILTER=proxy), register proxy tool definitions
|
|
1014
489
|
// and create the control API client for communicating with the MITM proxy.
|
|
1015
|
-
const isVirtualOnlyMode =
|
|
490
|
+
const isVirtualOnlyMode = serverFilter === 'proxy' && Object.keys(serversConfig).length === 0;
|
|
1016
491
|
let controlApiClient = null;
|
|
1017
492
|
if (isVirtualOnlyMode) {
|
|
1018
493
|
for (const toolDef of proxyToolDefinitions) {
|
|
@@ -1030,6 +505,11 @@ async function main() {
|
|
|
1030
505
|
}
|
|
1031
506
|
controlApiClient = createControlApiClient(mitmControlAddr);
|
|
1032
507
|
}
|
|
508
|
+
// Defense-in-depth: the relay's CallTool handler routes by bare
|
|
509
|
+
// tool name, so two backends exposing the same tool name would
|
|
510
|
+
// silently collide and route to the wrong server. Fail fast if we
|
|
511
|
+
// ever end up with such a collision in the loaded tool set.
|
|
512
|
+
assertUniqueToolNames(allTools);
|
|
1033
513
|
const toolMap = buildToolMap(allTools);
|
|
1034
514
|
const toolDescriptionHints = loadToolDescriptionHints();
|
|
1035
515
|
const hintedTools = applyToolDescriptionHints(allTools, toolDescriptionHints);
|
|
@@ -1044,23 +524,85 @@ async function main() {
|
|
|
1044
524
|
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional use of low-level Server for raw JSON schema passthrough
|
|
1045
525
|
const server = new Server({ name: 'ironcurtain-proxy', version: VERSION }, { capabilities: { tools: {} } });
|
|
1046
526
|
server.setRequestHandler(ListToolsRequestSchema, () => listToolsResponse);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
527
|
+
// ── Roots bridging (coordinator → relay → backends) ───────────────
|
|
528
|
+
// The coordinator in the parent process is the authoritative source
|
|
529
|
+
// of MCP roots. On escalation approval it expands its roots and
|
|
530
|
+
// sends `notifications/roots/list_changed` to this relay. We fetch
|
|
531
|
+
// the fresh list from the coordinator via `server.listRoots()`,
|
|
532
|
+
// replace `relayRoots` in place, and propagate the notification to
|
|
533
|
+
// every backend client so they re-query via their `roots/list`
|
|
534
|
+
// handlers (registered above).
|
|
535
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
536
|
+
try {
|
|
537
|
+
const { roots: updatedRoots } = await server.listRoots();
|
|
538
|
+
// Replace contents in-place so all backend clientStates -- which
|
|
539
|
+
// share the same array reference as `relayRoots` -- see the new
|
|
540
|
+
// set on their next `roots/list` call. MCP `Root.name` is
|
|
541
|
+
// optional in the spec; fall back to the URI so our internal
|
|
542
|
+
// `name: string` contract holds.
|
|
543
|
+
relayRoots.length = 0;
|
|
544
|
+
for (const r of updatedRoots) {
|
|
545
|
+
relayRoots.push({ uri: r.uri, name: r.name ?? r.uri });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
if (sessionLogPath) {
|
|
550
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
551
|
+
logToSessionFile(sessionLogPath, `[proxy] Failed to fetch roots from coordinator: ${msg}`);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
await Promise.all([...clientStates.values()].map(async (state) => {
|
|
556
|
+
try {
|
|
557
|
+
await state.client.sendRootsListChanged();
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
if (sessionLogPath) {
|
|
561
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
562
|
+
logToSessionFile(sessionLogPath, `[proxy] sendRootsListChanged to backend failed: ${msg}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}));
|
|
566
|
+
});
|
|
567
|
+
// Pass-through CallTool handler: forward to the backend client,
|
|
568
|
+
// return the raw result. No policy evaluation -- the coordinator
|
|
569
|
+
// in the parent process handles that.
|
|
1062
570
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1063
|
-
|
|
571
|
+
const toolInfo = toolMap.get(req.params.name);
|
|
572
|
+
if (!toolInfo) {
|
|
573
|
+
return {
|
|
574
|
+
content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
|
|
575
|
+
isError: true,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
// Virtual proxy tools stay local -- they speak to the MITM control
|
|
579
|
+
// socket via the ControlApiClient and never reach a backend MCP server.
|
|
580
|
+
const rawArgs = req.params.arguments ?? {};
|
|
581
|
+
if (toolInfo.serverName === 'proxy' && controlApiClient) {
|
|
582
|
+
try {
|
|
583
|
+
const result = await handleVirtualProxyTool(toolInfo.name, rawArgs, controlApiClient);
|
|
584
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
588
|
+
return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const clientState = clientStates.get(toolInfo.serverName);
|
|
592
|
+
if (!clientState) {
|
|
593
|
+
return {
|
|
594
|
+
content: [{ type: 'text', text: `Internal error: no client connection for server "${toolInfo.serverName}"` }],
|
|
595
|
+
isError: true,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const result = await clientState.client.callTool({ name: toolInfo.name, arguments: rawArgs }, CompatibilityCallToolResultSchema);
|
|
600
|
+
return result.isError ? { content: result.content, isError: true } : { content: result.content };
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
const errorMessage = extractMcpErrorMessage(err);
|
|
604
|
+
return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
|
|
605
|
+
}
|
|
1064
606
|
});
|
|
1065
607
|
// ── Transport selection ───────────────────────────────────────────
|
|
1066
608
|
const transportConfig = selectTransportConfig();
|
|
@@ -1105,7 +647,6 @@ async function main() {
|
|
|
1105
647
|
catch {
|
|
1106
648
|
/* ignore */
|
|
1107
649
|
}
|
|
1108
|
-
await auditLog.close();
|
|
1109
650
|
process.exit(0);
|
|
1110
651
|
}
|
|
1111
652
|
process.on('SIGINT', () => {
|