@provos/ironcurtain 0.9.1 → 0.11.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 +69 -10
- package/dist/auth/oauth-provider.js +1 -4
- package/dist/auth/oauth-provider.js.map +1 -1
- package/dist/cli.js +40 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.js +174 -10
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/first-start.js +61 -0
- 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 +8 -2
- package/dist/config/model-provider.js +52 -9
- package/dist/config/model-provider.js.map +1 -1
- package/dist/config/paths.d.ts +181 -0
- package/dist/config/paths.js +326 -23
- package/dist/config/paths.js.map +1 -1
- package/dist/config/types.d.ts +7 -1
- package/dist/config/user-config.d.ts +61 -4
- package/dist/config/user-config.js +108 -21
- 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/compile-task-policy.js +1 -0
- package/dist/cron/compile-task-policy.js.map +1 -1
- 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 +16 -2
- package/dist/daemon/daemon-command.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.d.ts +44 -1
- package/dist/daemon/ironcurtain-daemon.js +210 -8
- package/dist/daemon/ironcurtain-daemon.js.map +1 -1
- package/dist/docker/adapters/claude-code.d.ts +2 -1
- package/dist/docker/adapters/claude-code.js +320 -131
- package/dist/docker/adapters/claude-code.js.map +1 -1
- 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 +178 -4
- package/dist/docker/agent-adapter.js +14 -0
- package/dist/docker/agent-adapter.js.map +1 -1
- package/dist/docker/agent-registry.js +3 -2
- package/dist/docker/agent-registry.js.map +1 -1
- package/dist/docker/code-mode-proxy.d.ts +27 -0
- package/dist/docker/code-mode-proxy.js +3 -0
- package/dist/docker/code-mode-proxy.js.map +1 -1
- package/dist/docker/container-lifecycle.d.ts +15 -0
- package/dist/docker/container-lifecycle.js +32 -0
- package/dist/docker/container-lifecycle.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +113 -62
- package/dist/docker/docker-agent-session.js +241 -300
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +256 -21
- package/dist/docker/docker-infrastructure.js +585 -30
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.d.ts +36 -1
- package/dist/docker/docker-manager.js +168 -26
- package/dist/docker/docker-manager.js.map +1 -1
- 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 +90 -1
- package/dist/docker/mitm-proxy.js +720 -40
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/oauth-credentials.d.ts +46 -3
- package/dist/docker/oauth-credentials.js +56 -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/provider-config.d.ts +81 -2
- package/dist/docker/provider-config.js +222 -28
- package/dist/docker/provider-config.js.map +1 -1
- 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 +49 -1
- package/dist/docker/pty-session.js +212 -90
- 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/resource-limits.d.ts +125 -0
- package/dist/docker/resource-limits.js +200 -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/types.d.ts +61 -6
- package/dist/doctor/checks.d.ts +138 -0
- package/dist/doctor/checks.js +511 -0
- package/dist/doctor/checks.js.map +1 -0
- package/dist/doctor/doctor-command.d.ts +29 -0
- package/dist/doctor/doctor-command.js +160 -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 +84 -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 +32 -9
- 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 +30 -34
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.d.ts +10 -1
- package/dist/mux/mux-command.js +62 -35
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-renderer.js +22 -17
- 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.d.ts +7 -0
- package/dist/mux/session-scanner.js +21 -2
- package/dist/mux/session-scanner.js.map +1 -1
- package/dist/mux/types.d.ts +0 -4
- package/dist/mux/types.js.map +1 -1
- package/dist/{pipeline → observability}/llm-logger.d.ts +9 -3
- package/dist/{pipeline → observability}/llm-logger.js +16 -4
- package/dist/observability/llm-logger.js.map +1 -0
- package/dist/observe/observe-command.d.ts +12 -0
- package/dist/observe/observe-command.js +298 -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/compile-persona-policy.js +1 -0
- package/dist/persona/compile-persona-policy.js.map +1 -1
- 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 +41 -6
- package/dist/persona/persona-command.js.map +1 -1
- 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/types.d.ts +16 -0
- package/dist/pipeline/annotate.d.ts +7 -1
- package/dist/pipeline/annotate.js +92 -2
- package/dist/pipeline/annotate.js.map +1 -1
- package/dist/pipeline/compile.js +2 -0
- 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/parallel-progress.d.ts +59 -0
- package/dist/pipeline/parallel-progress.js +263 -0
- package/dist/pipeline/parallel-progress.js.map +1 -0
- package/dist/pipeline/pipeline-runner.d.ts +49 -8
- package/dist/pipeline/pipeline-runner.js +419 -231
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +59 -1
- package/dist/pipeline/pipeline-shared.js +127 -12
- 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/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/pipeline/server-prefilter.d.ts +31 -0
- package/dist/pipeline/server-prefilter.js +115 -0
- package/dist/pipeline/server-prefilter.js.map +1 -0
- package/dist/pipeline/types.d.ts +5 -0
- package/dist/sandbox/index.d.ts +14 -0
- package/dist/sandbox/index.js +335 -116
- 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 +324 -130
- package/dist/session/index.js.map +1 -1
- package/dist/session/preflight.d.ts +50 -13
- package/dist/session/preflight.js +252 -80
- 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 +311 -5
- package/dist/session/types.js +57 -1
- package/dist/session/types.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.js +7 -1
- 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.js +2 -1
- package/dist/trusted-process/call-circuit-breaker.js.map +1 -1
- 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 +166 -657
- 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 +90 -45
- 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 +26 -0
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/tool-call-coordinator.d.ts +269 -0
- package/dist/trusted-process/tool-call-coordinator.js +508 -0
- package/dist/trusted-process/tool-call-coordinator.js.map +1 -0
- package/dist/trusted-process/tool-call-pipeline.d.ts +205 -0
- package/dist/trusted-process/tool-call-pipeline.js +798 -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/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/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 +6 -0
- package/dist/web-ui/dispatch/persona-dispatch.js +116 -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 +201 -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 +31 -0
- package/dist/web-ui/dispatch/types.js +68 -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 +925 -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 +152 -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 +76 -0
- package/dist/web-ui/web-ui-server.js +461 -0
- package/dist/web-ui/web-ui-server.js.map +1 -0
- package/dist/web-ui/web-ui-types.d.ts +290 -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-BFO5z1js.js +76 -0
- package/dist/web-ui-static/assets/index-CCUwrkIL.css +1 -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-support.d.ts +92 -0
- package/dist/workflow/cli-support.js +423 -0
- package/dist/workflow/cli-support.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 +78 -0
- package/dist/workflow/discovery.js +203 -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/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 +66 -0
- package/dist/workflow/lint.js +410 -0
- package/dist/workflow/lint.js.map +1 -0
- package/dist/workflow/machine-builder.d.ts +90 -0
- package/dist/workflow/machine-builder.js +485 -0
- package/dist/workflow/machine-builder.js.map +1 -0
- package/dist/workflow/message-log.d.ts +85 -0
- package/dist/workflow/message-log.js +40 -0
- package/dist/workflow/message-log.js.map +1 -0
- package/dist/workflow/orchestrator.d.ts +450 -0
- package/dist/workflow/orchestrator.js +1941 -0
- package/dist/workflow/orchestrator.js.map +1 -0
- package/dist/workflow/prompt-builder.d.ts +39 -0
- package/dist/workflow/prompt-builder.js +242 -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 +259 -0
- package/dist/workflow/status-parser.js.map +1 -0
- package/dist/workflow/types.d.ts +498 -0
- package/dist/workflow/types.js +83 -0
- package/dist/workflow/types.js.map +1 -0
- package/dist/workflow/validate.d.ts +58 -0
- package/dist/workflow/validate.js +499 -0
- package/dist/workflow/validate.js.map +1 -0
- package/dist/workflow/workflow-command.d.ts +26 -0
- package/dist/workflow/workflow-command.js +648 -0
- package/dist/workflow/workflow-command.js.map +1 -0
- package/dist/workflow/workflow-discovery.d.ts +25 -0
- package/dist/workflow/workflow-discovery.js +100 -0
- package/dist/workflow/workflow-discovery.js.map +1 -0
- package/dist/workflow/workflow-manager.d.ts +122 -0
- package/dist/workflow/workflow-manager.js +308 -0
- package/dist/workflow/workflow-manager.js.map +1 -0
- package/dist/workflow/workflows/design-and-code/workflow.yaml +169 -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 +89 -0
- package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
- package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
- package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
- package/dist/workflow/workflows/vuln-discovery/workflow.yaml +920 -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 +12 -1
- package/docker/Dockerfile.base.arm64 +17 -1
- package/docker/entrypoint-claude-code.sh +46 -0
- package/docker/entrypoint-goose.sh +25 -0
- package/package.json +22 -8
- 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/workflow.yaml +169 -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 +89 -0
- package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +201 -0
- package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +166 -0
- package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +185 -0
- package/src/workflow/workflows/vuln-discovery/workflow.yaml +920 -0
- package/dist/pipeline/llm-logger.js.map +0 -1
- package/dist/session/errors.js.map +0 -1
|
@@ -1,65 +1,53 @@
|
|
|
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 { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequestSchema, ListRootsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
36
|
+
import { CallToolRequestSchema, CompatibilityCallToolResultSchema, ListToolsRequestSchema, ListRootsRequestSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
34
37
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
35
38
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
36
|
-
import { appendFileSync,
|
|
37
|
-
import { atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
|
|
39
|
+
import { appendFileSync, mkdtempSync, writeFileSync, writeSync } from 'node:fs';
|
|
38
40
|
import { join, resolve } from 'node:path';
|
|
39
41
|
import { fileURLToPath } from 'node:url';
|
|
40
42
|
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';
|
|
43
|
+
import { loadGeneratedPolicy, getPackageGeneratedDir } from '../config/index.js';
|
|
44
|
+
import { expandTilde } from '../types/argument-roles.js';
|
|
45
|
+
import { extractPolicyRoots, toMcpRoots } from './policy-roots.js';
|
|
46
|
+
import { checkSandboxAvailability, resolveSandboxConfig, writeServerSettings, wrapServerCommand, cleanupSettingsFiles, discoverNodePaths, rewriteServerSettings, } from './sandbox-integration.js';
|
|
56
47
|
import { extractMcpErrorMessage } from './mcp-error-utils.js';
|
|
57
|
-
import { updateServerContext, formatServerContext } from './server-context.js';
|
|
58
48
|
import { permissiveJsonSchemaValidator } from './permissive-output-validator.js';
|
|
59
49
|
import { VERSION } from '../version.js';
|
|
60
|
-
import { buildTrustedServerSet } from '../memory/memory-annotations.js';
|
|
61
50
|
import { loadToolDescriptionHints, applyToolDescriptionHints } from './tool-description-hints.js';
|
|
62
|
-
import { createApprovalWhitelist, extractWhitelistCandidates, } from './approval-whitelist.js';
|
|
63
51
|
import { getProviderForServer } from '../auth/oauth-registry.js';
|
|
64
52
|
import { loadClientCredentials } from '../auth/oauth-provider.js';
|
|
65
53
|
import { OAuthTokenProvider } from '../auth/oauth-token-provider.js';
|
|
@@ -67,68 +55,10 @@ import { loadOAuthToken } from '../auth/oauth-token-store.js';
|
|
|
67
55
|
import { writeGWorkspaceCredentialFile } from './gworkspace-credentials.js';
|
|
68
56
|
import { TokenFileRefresher } from './token-file-refresher.js';
|
|
69
57
|
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
|
-
}
|
|
58
|
+
import { buildToolMap, assertUniqueToolNames } from './tool-call-pipeline.js';
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Subprocess helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
132
62
|
/** Appends a timestamped line to the session log file. */
|
|
133
63
|
function logToSessionFile(sessionLogPath, message) {
|
|
134
64
|
const timestamp = new Date().toISOString();
|
|
@@ -157,15 +87,6 @@ function getMissingEnvVars(args) {
|
|
|
157
87
|
}
|
|
158
88
|
return missing;
|
|
159
89
|
}
|
|
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
90
|
/**
|
|
170
91
|
* Replaces known credential values in a string with `***REDACTED***`.
|
|
171
92
|
* Prevents credential leakage in session log files.
|
|
@@ -179,163 +100,40 @@ function redactCredentials(text, credentials) {
|
|
|
179
100
|
}
|
|
180
101
|
return result;
|
|
181
102
|
}
|
|
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
103
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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)
|
|
104
|
+
* True when this proxy subprocess is in a viable serving state -- either
|
|
105
|
+
* virtual-only mode (no backends configured) or at least one configured
|
|
106
|
+
* backend connected. Returning false means the parent should treat this
|
|
107
|
+
* subprocess as failed; otherwise it would register zero tools for the
|
|
108
|
+
* server and trigger a misleading "stale annotations" warning in the
|
|
109
|
+
* coordinator's drift check.
|
|
202
110
|
*/
|
|
203
|
-
export function
|
|
204
|
-
|
|
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
|
-
/**
|
|
238
|
-
* Removes both escalation IPC files. Request is deleted first so the
|
|
239
|
-
* session side sees "request gone + response exists" as normal
|
|
240
|
-
* approval-in-progress rather than an expiry.
|
|
241
|
-
*/
|
|
242
|
-
function cleanupEscalationFiles(requestPath, responsePath) {
|
|
243
|
-
tryUnlink(requestPath);
|
|
244
|
-
tryUnlink(responsePath);
|
|
111
|
+
export function proxyCanServe(serversConfig, connectedBackendCount) {
|
|
112
|
+
return Object.keys(serversConfig).length === 0 || connectedBackendCount > 0;
|
|
245
113
|
}
|
|
246
114
|
/**
|
|
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.
|
|
115
|
+
* Writes a diagnostic line to stderr and (if configured) the proxy session log.
|
|
270
116
|
*
|
|
271
|
-
*
|
|
117
|
+
* Uses `fs.writeSync` so the bytes reach the OS before the function returns
|
|
118
|
+
* even when stderr is a pipe (parent-spawned subprocess). `process.stderr.write`
|
|
119
|
+
* is asynchronous in that case, which would let a follow-up `process.exit(1)`
|
|
120
|
+
* truncate the diagnostic before delivery.
|
|
272
121
|
*/
|
|
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' };
|
|
122
|
+
function emitProxyDiagnostic(level, message, sessionLogPath) {
|
|
123
|
+
writeSync(2, `${level}: ${message}\n`);
|
|
124
|
+
if (sessionLogPath)
|
|
125
|
+
logToSessionFile(sessionLogPath, `[proxy] ${message}`);
|
|
290
126
|
}
|
|
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
127
|
/**
|
|
328
128
|
* Reads and validates proxy environment variables.
|
|
329
129
|
* Returns a typed config object or calls process.exit(1) on missing required vars.
|
|
330
130
|
*/
|
|
331
131
|
export function parseProxyEnvConfig() {
|
|
332
|
-
const auditLogPath = process.env.AUDIT_LOG_PATH ?? './audit.jsonl';
|
|
333
132
|
const serversConfigJson = process.env.MCP_SERVERS_CONFIG;
|
|
334
133
|
const generatedDir = process.env.GENERATED_DIR;
|
|
335
134
|
const protectedPathsJson = process.env.PROTECTED_PATHS ?? '[]';
|
|
336
135
|
const sessionLogPath = process.env.SESSION_LOG_PATH;
|
|
337
136
|
const allowedDirectory = process.env.ALLOWED_DIRECTORY;
|
|
338
|
-
const escalationDir = process.env.ESCALATION_DIR;
|
|
339
137
|
if (!serversConfigJson) {
|
|
340
138
|
process.stderr.write('MCP_SERVERS_CONFIG environment variable is required\n');
|
|
341
139
|
process.exit(1);
|
|
@@ -351,7 +149,6 @@ export function parseProxyEnvConfig() {
|
|
|
351
149
|
: {};
|
|
352
150
|
delete process.env.SERVER_CREDENTIALS;
|
|
353
151
|
const sandboxPolicy = (process.env.SANDBOX_POLICY ?? 'warn');
|
|
354
|
-
const auditRedaction = process.env.AUDIT_REDACTION === 'true';
|
|
355
152
|
const allServersConfig = JSON.parse(serversConfigJson);
|
|
356
153
|
const protectedPaths = JSON.parse(protectedPathsJson);
|
|
357
154
|
// When SERVER_FILTER is set, only connect to that single backend server.
|
|
@@ -373,17 +170,15 @@ export function parseProxyEnvConfig() {
|
|
|
373
170
|
// Otherwise they default to the same directory as compiled policy.
|
|
374
171
|
const toolAnnotationsDir = process.env.TOOL_ANNOTATIONS_DIR ?? generatedDir;
|
|
375
172
|
return {
|
|
376
|
-
auditLogPath,
|
|
377
173
|
serversConfig,
|
|
378
174
|
generatedDir,
|
|
379
175
|
toolAnnotationsDir,
|
|
380
176
|
protectedPaths,
|
|
381
177
|
sessionLogPath,
|
|
382
178
|
allowedDirectory,
|
|
383
|
-
escalationDir,
|
|
384
179
|
serverCredentials,
|
|
385
180
|
sandboxPolicy,
|
|
386
|
-
|
|
181
|
+
serverFilter,
|
|
387
182
|
};
|
|
388
183
|
}
|
|
389
184
|
/**
|
|
@@ -433,365 +228,6 @@ export function resolveServerSandboxConfigs(serversConfig, allowedDirectory, san
|
|
|
433
228
|
}
|
|
434
229
|
return { resolvedSandboxConfigs, settingsDir, serverCwdPaths };
|
|
435
230
|
}
|
|
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
231
|
/**
|
|
796
232
|
* Selects and validates the proxy transport based on environment variables.
|
|
797
233
|
* Returns 'tcp', 'uds', or 'stdio' along with the transport options.
|
|
@@ -814,27 +250,30 @@ export function selectTransportConfig() {
|
|
|
814
250
|
// ── main() -- thin orchestrator ────────────────────────────────────────
|
|
815
251
|
async function main() {
|
|
816
252
|
const envConfig = parseProxyEnvConfig();
|
|
817
|
-
const {
|
|
818
|
-
|
|
253
|
+
const { serversConfig, generatedDir, toolAnnotationsDir, sessionLogPath, allowedDirectory, serverCredentials, sandboxPolicy, serverFilter, } = envConfig;
|
|
254
|
+
// Load compiled policy + annotations to derive MCP roots advertised
|
|
255
|
+
// to backend MCP servers at connection time (the backend still uses
|
|
256
|
+
// Roots to authorize filesystem access).
|
|
257
|
+
const { compiledPolicy, toolAnnotations } = loadGeneratedPolicy({
|
|
819
258
|
policyDir: generatedDir,
|
|
820
259
|
toolAnnotationsDir,
|
|
821
260
|
fallbackDir: getPackageGeneratedDir(),
|
|
822
261
|
});
|
|
823
|
-
// Merge proxy tool annotations and policy rules
|
|
824
|
-
//
|
|
262
|
+
// Merge proxy tool annotations and policy rules so roots derivation
|
|
263
|
+
// includes any paths referenced by proxy policy rules.
|
|
825
264
|
toolAnnotations.servers.proxy = {
|
|
826
265
|
inputHash: 'hardcoded',
|
|
827
266
|
tools: proxyAnnotations,
|
|
828
267
|
};
|
|
829
268
|
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
269
|
const policyRoots = extractPolicyRoots(compiledPolicy, allowedDirectory ?? '/tmp');
|
|
837
270
|
const mcpRoots = toMcpRoots(policyRoots);
|
|
271
|
+
// Shared mutable roots array bridged to the coordinator. The proxy
|
|
272
|
+
// server's `notifications/roots/list_changed` handler (registered
|
|
273
|
+
// below) refreshes this in place via `server.listRoots()`, and each
|
|
274
|
+
// backend client's `roots/list` handler returns it. All backends
|
|
275
|
+
// observe the same authoritative set.
|
|
276
|
+
const relayRoots = [...mcpRoots];
|
|
838
277
|
// ── Sandbox availability & config resolution ──────────────────────
|
|
839
278
|
const { sandboxAvailable } = validateSandboxAvailability(sandboxPolicy, sessionLogPath, process.platform);
|
|
840
279
|
const { resolvedSandboxConfigs, settingsDir, serverCwdPaths } = resolveServerSandboxConfigs(serversConfig, allowedDirectory, sandboxAvailable, sandboxPolicy);
|
|
@@ -849,10 +288,7 @@ async function main() {
|
|
|
849
288
|
// that aren't set — the server will fail to start without them.
|
|
850
289
|
const missingEnvVars = getMissingEnvVars(config.args);
|
|
851
290
|
if (missingEnvVars.length > 0) {
|
|
852
|
-
|
|
853
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
854
|
-
if (sessionLogPath)
|
|
855
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
291
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": missing environment variable(s) ${missingEnvVars.join(', ')}`, sessionLogPath);
|
|
856
292
|
continue;
|
|
857
293
|
}
|
|
858
294
|
// ── OAuth token injection for servers backed by OAuth providers ──
|
|
@@ -861,18 +297,12 @@ async function main() {
|
|
|
861
297
|
if (oauthProvider) {
|
|
862
298
|
const clientCreds = loadClientCredentials(oauthProvider);
|
|
863
299
|
if (!clientCreds) {
|
|
864
|
-
|
|
865
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
866
|
-
if (sessionLogPath)
|
|
867
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
300
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": no OAuth credentials. Run 'ironcurtain auth import ${oauthProvider.id} <file>'`, sessionLogPath);
|
|
868
301
|
continue;
|
|
869
302
|
}
|
|
870
303
|
const tokenProvider = new OAuthTokenProvider(oauthProvider, clientCreds);
|
|
871
304
|
if (!tokenProvider.isAuthorized()) {
|
|
872
|
-
|
|
873
|
-
process.stderr.write(`WARNING: ${warning}\n`);
|
|
874
|
-
if (sessionLogPath)
|
|
875
|
-
logToSessionFile(sessionLogPath, `[proxy] ${warning}`);
|
|
305
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": not authorized. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
|
|
876
306
|
continue;
|
|
877
307
|
}
|
|
878
308
|
try {
|
|
@@ -928,10 +358,7 @@ async function main() {
|
|
|
928
358
|
}
|
|
929
359
|
catch (err) {
|
|
930
360
|
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}`);
|
|
361
|
+
emitProxyDiagnostic('WARNING', `Skipping MCP server "${serverName}": OAuth token error: ${message}. Run 'ironcurtain auth ${oauthProvider.id}'`, sessionLogPath);
|
|
935
362
|
continue;
|
|
936
363
|
}
|
|
937
364
|
}
|
|
@@ -983,7 +410,11 @@ async function main() {
|
|
|
983
410
|
capabilities: { roots: { listChanged: true } },
|
|
984
411
|
jsonSchemaValidator: permissiveJsonSchemaValidator,
|
|
985
412
|
});
|
|
986
|
-
|
|
413
|
+
// All backend clients share the single `relayRoots` array so a
|
|
414
|
+
// coordinator-driven roots update (see the proxy server's
|
|
415
|
+
// `roots/list_changed` handler below) is visible to every backend
|
|
416
|
+
// on its next `roots/list` request.
|
|
417
|
+
const state = { client, roots: relayRoots };
|
|
987
418
|
client.setRequestHandler(ListRootsRequestSchema, () => {
|
|
988
419
|
if (state.rootsRefreshed) {
|
|
989
420
|
state.rootsRefreshed();
|
|
@@ -1010,9 +441,21 @@ async function main() {
|
|
|
1010
441
|
});
|
|
1011
442
|
}
|
|
1012
443
|
}
|
|
444
|
+
// Exit non-zero so the parent's `MCPClientManager.connect()` fails the
|
|
445
|
+
// handshake -- the existing try/catch in `connectBackendSubprocesses`
|
|
446
|
+
// skips the server without reaching `registerTools`.
|
|
447
|
+
if (!proxyCanServe(serversConfig, clientStates.size)) {
|
|
448
|
+
const serverNames = Object.keys(serversConfig).join(', ');
|
|
449
|
+
emitProxyDiagnostic('ERROR', `proxy subprocess for SERVER_FILTER="${serverFilter ?? '<unset>'}" has no connected backend (configured: ${serverNames}); exiting`, sessionLogPath);
|
|
450
|
+
for (const refresher of tokenRefreshers.values()) {
|
|
451
|
+
refresher.stop();
|
|
452
|
+
}
|
|
453
|
+
cleanupSettingsFiles(settingsDir);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
1013
456
|
// In virtual-only mode (SERVER_FILTER=proxy), register proxy tool definitions
|
|
1014
457
|
// and create the control API client for communicating with the MITM proxy.
|
|
1015
|
-
const isVirtualOnlyMode =
|
|
458
|
+
const isVirtualOnlyMode = serverFilter === 'proxy' && Object.keys(serversConfig).length === 0;
|
|
1016
459
|
let controlApiClient = null;
|
|
1017
460
|
if (isVirtualOnlyMode) {
|
|
1018
461
|
for (const toolDef of proxyToolDefinitions) {
|
|
@@ -1030,6 +473,11 @@ async function main() {
|
|
|
1030
473
|
}
|
|
1031
474
|
controlApiClient = createControlApiClient(mitmControlAddr);
|
|
1032
475
|
}
|
|
476
|
+
// Defense-in-depth: the relay's CallTool handler routes by bare
|
|
477
|
+
// tool name, so two backends exposing the same tool name would
|
|
478
|
+
// silently collide and route to the wrong server. Fail fast if we
|
|
479
|
+
// ever end up with such a collision in the loaded tool set.
|
|
480
|
+
assertUniqueToolNames(allTools);
|
|
1033
481
|
const toolMap = buildToolMap(allTools);
|
|
1034
482
|
const toolDescriptionHints = loadToolDescriptionHints();
|
|
1035
483
|
const hintedTools = applyToolDescriptionHints(allTools, toolDescriptionHints);
|
|
@@ -1044,23 +492,85 @@ async function main() {
|
|
|
1044
492
|
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional use of low-level Server for raw JSON schema passthrough
|
|
1045
493
|
const server = new Server({ name: 'ironcurtain-proxy', version: VERSION }, { capabilities: { tools: {} } });
|
|
1046
494
|
server.setRequestHandler(ListToolsRequestSchema, () => listToolsResponse);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
495
|
+
// ── Roots bridging (coordinator → relay → backends) ───────────────
|
|
496
|
+
// The coordinator in the parent process is the authoritative source
|
|
497
|
+
// of MCP roots. On escalation approval it expands its roots and
|
|
498
|
+
// sends `notifications/roots/list_changed` to this relay. We fetch
|
|
499
|
+
// the fresh list from the coordinator via `server.listRoots()`,
|
|
500
|
+
// replace `relayRoots` in place, and propagate the notification to
|
|
501
|
+
// every backend client so they re-query via their `roots/list`
|
|
502
|
+
// handlers (registered above).
|
|
503
|
+
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
|
504
|
+
try {
|
|
505
|
+
const { roots: updatedRoots } = await server.listRoots();
|
|
506
|
+
// Replace contents in-place so all backend clientStates -- which
|
|
507
|
+
// share the same array reference as `relayRoots` -- see the new
|
|
508
|
+
// set on their next `roots/list` call. MCP `Root.name` is
|
|
509
|
+
// optional in the spec; fall back to the URI so our internal
|
|
510
|
+
// `name: string` contract holds.
|
|
511
|
+
relayRoots.length = 0;
|
|
512
|
+
for (const r of updatedRoots) {
|
|
513
|
+
relayRoots.push({ uri: r.uri, name: r.name ?? r.uri });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
if (sessionLogPath) {
|
|
518
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
519
|
+
logToSessionFile(sessionLogPath, `[proxy] Failed to fetch roots from coordinator: ${msg}`);
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
await Promise.all([...clientStates.values()].map(async (state) => {
|
|
524
|
+
try {
|
|
525
|
+
await state.client.sendRootsListChanged();
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
if (sessionLogPath) {
|
|
529
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
+
logToSessionFile(sessionLogPath, `[proxy] sendRootsListChanged to backend failed: ${msg}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}));
|
|
534
|
+
});
|
|
535
|
+
// Pass-through CallTool handler: forward to the backend client,
|
|
536
|
+
// return the raw result. No policy evaluation -- the coordinator
|
|
537
|
+
// in the parent process handles that.
|
|
1062
538
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1063
|
-
|
|
539
|
+
const toolInfo = toolMap.get(req.params.name);
|
|
540
|
+
if (!toolInfo) {
|
|
541
|
+
return {
|
|
542
|
+
content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
|
|
543
|
+
isError: true,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// Virtual proxy tools stay local -- they speak to the MITM control
|
|
547
|
+
// socket via the ControlApiClient and never reach a backend MCP server.
|
|
548
|
+
const rawArgs = req.params.arguments ?? {};
|
|
549
|
+
if (toolInfo.serverName === 'proxy' && controlApiClient) {
|
|
550
|
+
try {
|
|
551
|
+
const result = await handleVirtualProxyTool(toolInfo.name, rawArgs, controlApiClient);
|
|
552
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
|
|
553
|
+
}
|
|
554
|
+
catch (err) {
|
|
555
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
556
|
+
return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const clientState = clientStates.get(toolInfo.serverName);
|
|
560
|
+
if (!clientState) {
|
|
561
|
+
return {
|
|
562
|
+
content: [{ type: 'text', text: `Internal error: no client connection for server "${toolInfo.serverName}"` }],
|
|
563
|
+
isError: true,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const result = await clientState.client.callTool({ name: toolInfo.name, arguments: rawArgs }, CompatibilityCallToolResultSchema);
|
|
568
|
+
return result.isError ? { content: result.content, isError: true } : { content: result.content };
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
const errorMessage = extractMcpErrorMessage(err);
|
|
572
|
+
return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true };
|
|
573
|
+
}
|
|
1064
574
|
});
|
|
1065
575
|
// ── Transport selection ───────────────────────────────────────────
|
|
1066
576
|
const transportConfig = selectTransportConfig();
|
|
@@ -1105,7 +615,6 @@ async function main() {
|
|
|
1105
615
|
catch {
|
|
1106
616
|
/* ignore */
|
|
1107
617
|
}
|
|
1108
|
-
await auditLog.close();
|
|
1109
618
|
process.exit(0);
|
|
1110
619
|
}
|
|
1111
620
|
process.on('SIGINT', () => {
|