@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
|
@@ -19,10 +19,14 @@ import * as net from 'node:net';
|
|
|
19
19
|
import { existsSync, unlinkSync } from 'node:fs';
|
|
20
20
|
import forge from 'node-forge';
|
|
21
21
|
import { randomSerialNumber } from './ca.js';
|
|
22
|
-
import { isEndpointAllowed, shouldRewriteBody, } from './provider-config.js';
|
|
22
|
+
import { isCapturableEndpoint, isEndpointAllowed, shouldRewriteBody, } from './provider-config.js';
|
|
23
23
|
import { DENY_ALL_VALIDATOR } from './package-validator.js';
|
|
24
24
|
import { validateDomain } from './proxy-tools.js';
|
|
25
|
+
import { PassThrough } from 'node:stream';
|
|
26
|
+
import { getTokenStreamBus } from './token-stream-bus.js';
|
|
27
|
+
import { SseExtractorTransform } from './sse-extractor.js';
|
|
25
28
|
import * as logger from '../logger.js';
|
|
29
|
+
import { beginCaptureExchange, createResponseCaptureInlet } from './trajectory-tap.js';
|
|
26
30
|
/** Connection reset errors are routine during proxy shutdown or client disconnect. */
|
|
27
31
|
function isConnectionReset(err) {
|
|
28
32
|
return err.code === 'ECONNRESET' || err.code === 'EPIPE';
|
|
@@ -30,6 +34,47 @@ function isConnectionReset(err) {
|
|
|
30
34
|
const MAX_REWRITE_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
31
35
|
/** Max Content-Length for which we buffer solely for 401 retry (1 MB). */
|
|
32
36
|
const MAX_RETRY_BODY_BYTES = 1 * 1024 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* Max bytes buffered from a non-streaming JSON response body for token stream
|
|
39
|
+
* extraction. The response is still streamed to the client unchanged; only
|
|
40
|
+
* the extraction side stops capturing once the cap is hit. This protects the
|
|
41
|
+
* proxy from unbounded memory growth on very large completions.
|
|
42
|
+
*/
|
|
43
|
+
export const MAX_JSON_RESPONSE_CAPTURE_BYTES = 2 * 1024 * 1024; // 2 MB
|
|
44
|
+
/**
|
|
45
|
+
* Create a size-bounded capture for a JSON response body.
|
|
46
|
+
*
|
|
47
|
+
* Up to `maxBytes` of data is accumulated into an internal chunk array.
|
|
48
|
+
* Once that threshold is crossed, any already-buffered chunks are released
|
|
49
|
+
* (so large responses do not pin memory) and further chunks are ignored.
|
|
50
|
+
* The caller is still responsible for piping the stream through to the
|
|
51
|
+
* client -- this helper only manages the extraction-side buffer.
|
|
52
|
+
*/
|
|
53
|
+
export function createBoundedJsonResponseCapture(maxBytes = MAX_JSON_RESPONSE_CAPTURE_BYTES) {
|
|
54
|
+
let chunks = [];
|
|
55
|
+
let capturedBytes = 0;
|
|
56
|
+
return {
|
|
57
|
+
onData(chunk) {
|
|
58
|
+
if (capturedBytes > maxBytes)
|
|
59
|
+
return;
|
|
60
|
+
capturedBytes += chunk.length;
|
|
61
|
+
if (capturedBytes > maxBytes) {
|
|
62
|
+
chunks = [];
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
chunks.push(chunk);
|
|
66
|
+
},
|
|
67
|
+
onEnd(onComplete) {
|
|
68
|
+
if (capturedBytes > maxBytes) {
|
|
69
|
+
onComplete(null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const body = Buffer.concat(chunks);
|
|
73
|
+
chunks = [];
|
|
74
|
+
onComplete(body);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
33
78
|
/**
|
|
34
79
|
* Buffers the entire request body into a single Buffer.
|
|
35
80
|
* Rejects if the body exceeds maxBytes.
|
|
@@ -52,17 +97,262 @@ function bufferRequestBody(req, maxBytes) {
|
|
|
52
97
|
req.on('error', reject);
|
|
53
98
|
});
|
|
54
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Maps a target hostname to the SSE provider for token stream parsing.
|
|
102
|
+
* Only Anthropic and OpenAI hosts are recognized; everything else
|
|
103
|
+
* falls back to 'unknown'.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveSseProvider(hostname) {
|
|
106
|
+
if (hostname === 'api.anthropic.com' || hostname === 'platform.claude.com') {
|
|
107
|
+
return 'anthropic';
|
|
108
|
+
}
|
|
109
|
+
if (hostname === 'api.openai.com') {
|
|
110
|
+
return 'openai';
|
|
111
|
+
}
|
|
112
|
+
return 'unknown';
|
|
113
|
+
}
|
|
114
|
+
/** Maximum characters for tool_result content before truncation. */
|
|
115
|
+
const MAX_TOOL_RESULT_CONTENT_LEN = 500;
|
|
116
|
+
/** Truncate tool result content to MAX_TOOL_RESULT_CONTENT_LEN, appending ellipsis if needed. */
|
|
117
|
+
function truncateToolResult(text) {
|
|
118
|
+
return text.length > MAX_TOOL_RESULT_CONTENT_LEN ? text.slice(0, MAX_TOOL_RESULT_CONTENT_LEN) + '\u2026' : text;
|
|
119
|
+
}
|
|
120
|
+
/** Returns true for LLM messages endpoints that carry multi-turn request bodies. */
|
|
121
|
+
function isLlmMessagesEndpoint(path) {
|
|
122
|
+
const p = path.split('?')[0];
|
|
123
|
+
return p === '/v1/messages' || p === '/v1/chat/completions';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Extract tool_result blocks from a parsed Anthropic/OpenAI request body
|
|
127
|
+
* and push them as TokenStreamEvents to the bus.
|
|
128
|
+
*
|
|
129
|
+
* Anthropic format: messages[].role === 'user', content[].type === 'tool_result'
|
|
130
|
+
* OpenAI format: messages[].role === 'tool', with tool_call_id
|
|
131
|
+
*
|
|
132
|
+
* Only extracts from the trailing user/tool messages to avoid re-emitting
|
|
133
|
+
* historical tool results (the full conversation history is sent with every request).
|
|
134
|
+
*/
|
|
135
|
+
function extractTrailingToolMessages(messages) {
|
|
136
|
+
// Walk backward from the end to find the contiguous block of
|
|
137
|
+
// user (with tool_result) and tool messages at the tail.
|
|
138
|
+
const trailing = [];
|
|
139
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
140
|
+
const msg = messages[i];
|
|
141
|
+
if (typeof msg !== 'object' || msg === null)
|
|
142
|
+
break;
|
|
143
|
+
const role = msg['role'];
|
|
144
|
+
if (role === 'user' || role === 'tool') {
|
|
145
|
+
trailing.push(msg);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
trailing.reverse();
|
|
152
|
+
return trailing;
|
|
153
|
+
}
|
|
154
|
+
export function extractToolResults(parsed, sessionId) {
|
|
155
|
+
const messages = parsed['messages'];
|
|
156
|
+
if (!Array.isArray(messages))
|
|
157
|
+
return;
|
|
158
|
+
// Fetch the singleton bus lazily so `resetTokenStreamBus()` between tests
|
|
159
|
+
// is honored at call time (not import time).
|
|
160
|
+
const bus = getTokenStreamBus();
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
// Only extract from the LAST user/tool messages to avoid re-emitting
|
|
163
|
+
// historical tool results from earlier turns (the full conversation
|
|
164
|
+
// history is sent with every API request).
|
|
165
|
+
const lastMessages = extractTrailingToolMessages(messages);
|
|
166
|
+
for (const msg of lastMessages) {
|
|
167
|
+
if (typeof msg !== 'object' || msg === null)
|
|
168
|
+
continue;
|
|
169
|
+
const record = msg;
|
|
170
|
+
// Anthropic format: role=user, content array with tool_result blocks
|
|
171
|
+
if (record['role'] === 'user') {
|
|
172
|
+
const content = record['content'];
|
|
173
|
+
if (!Array.isArray(content))
|
|
174
|
+
continue;
|
|
175
|
+
for (const block of content) {
|
|
176
|
+
if (typeof block !== 'object' || block === null)
|
|
177
|
+
continue;
|
|
178
|
+
const b = block;
|
|
179
|
+
if (b['type'] !== 'tool_result')
|
|
180
|
+
continue;
|
|
181
|
+
const toolUseId = typeof b['tool_use_id'] === 'string' ? b['tool_use_id'] : '';
|
|
182
|
+
const isError = b['is_error'] === true;
|
|
183
|
+
const rawContent = b['content'];
|
|
184
|
+
let text = '';
|
|
185
|
+
if (typeof rawContent === 'string') {
|
|
186
|
+
text = rawContent;
|
|
187
|
+
}
|
|
188
|
+
else if (Array.isArray(rawContent)) {
|
|
189
|
+
// Content can be an array of {type: 'text', text: '...'} blocks
|
|
190
|
+
const parts = [];
|
|
191
|
+
for (const part of rawContent) {
|
|
192
|
+
if (typeof part === 'object' && part !== null && part['type'] === 'text') {
|
|
193
|
+
const t = part['text'];
|
|
194
|
+
if (typeof t === 'string')
|
|
195
|
+
parts.push(t);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
text = parts.join('\n');
|
|
199
|
+
}
|
|
200
|
+
text = truncateToolResult(text);
|
|
201
|
+
bus.push(sessionId, {
|
|
202
|
+
kind: 'tool_result',
|
|
203
|
+
toolUseId,
|
|
204
|
+
toolName: '',
|
|
205
|
+
content: text,
|
|
206
|
+
isError,
|
|
207
|
+
timestamp: now,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
// OpenAI format: role=tool, content is a string
|
|
213
|
+
if (record['role'] === 'tool') {
|
|
214
|
+
const toolCallId = typeof record['tool_call_id'] === 'string' ? record['tool_call_id'] : '';
|
|
215
|
+
let text = typeof record['content'] === 'string' ? record['content'] : '';
|
|
216
|
+
const isError = false; // OpenAI doesn't have an is_error field
|
|
217
|
+
text = truncateToolResult(text);
|
|
218
|
+
bus.push(sessionId, {
|
|
219
|
+
kind: 'tool_result',
|
|
220
|
+
toolUseId: toolCallId,
|
|
221
|
+
toolName: '',
|
|
222
|
+
content: text,
|
|
223
|
+
isError,
|
|
224
|
+
timestamp: now,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Extract token stream events from a non-streaming JSON response body.
|
|
231
|
+
* Used when the upstream returns application/json instead of text/event-stream.
|
|
232
|
+
*
|
|
233
|
+
* Emits message_start (model), text_delta (content), and message_end (usage).
|
|
234
|
+
*/
|
|
235
|
+
export function extractFromJsonResponse(body, sessionId) {
|
|
236
|
+
let parsed;
|
|
237
|
+
try {
|
|
238
|
+
parsed = JSON.parse(body.toString());
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return; // Not valid JSON -- nothing to extract
|
|
242
|
+
}
|
|
243
|
+
// Fetch the singleton bus lazily so `resetTokenStreamBus()` between tests
|
|
244
|
+
// is honored at call time (not import time).
|
|
245
|
+
const bus = getTokenStreamBus();
|
|
246
|
+
const now = Date.now();
|
|
247
|
+
// Anthropic format: { model, content: [{type: 'text', text: '...'}], usage: {input_tokens, output_tokens}, stop_reason }
|
|
248
|
+
const model = typeof parsed['model'] === 'string' ? parsed['model'] : null;
|
|
249
|
+
if (model) {
|
|
250
|
+
bus.push(sessionId, { kind: 'message_start', model, timestamp: now });
|
|
251
|
+
}
|
|
252
|
+
// Extract text from content blocks (Anthropic) or choices (OpenAI)
|
|
253
|
+
const content = parsed['content'];
|
|
254
|
+
if (Array.isArray(content)) {
|
|
255
|
+
// Anthropic: content is an array of blocks
|
|
256
|
+
for (const block of content) {
|
|
257
|
+
if (typeof block === 'object' && block !== null) {
|
|
258
|
+
const b = block;
|
|
259
|
+
if (b['type'] === 'text' && typeof b['text'] === 'string') {
|
|
260
|
+
bus.push(sessionId, { kind: 'text_delta', text: b['text'], timestamp: now });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const choices = parsed['choices'];
|
|
266
|
+
if (Array.isArray(choices)) {
|
|
267
|
+
// OpenAI: choices[].message.content
|
|
268
|
+
for (const choice of choices) {
|
|
269
|
+
if (typeof choice === 'object' && choice !== null) {
|
|
270
|
+
const c = choice;
|
|
271
|
+
const message = c['message'];
|
|
272
|
+
if (typeof message === 'object' && message !== null) {
|
|
273
|
+
const m = message;
|
|
274
|
+
if (typeof m['content'] === 'string') {
|
|
275
|
+
bus.push(sessionId, { kind: 'text_delta', text: m['content'], timestamp: now });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Usage and stop reason
|
|
282
|
+
const usage = parsed['usage'];
|
|
283
|
+
const stopReason = typeof parsed['stop_reason'] === 'string' ? parsed['stop_reason'] : 'stop';
|
|
284
|
+
let inputTokens = 0;
|
|
285
|
+
let outputTokens = 0;
|
|
286
|
+
if (typeof usage === 'object' && usage !== null) {
|
|
287
|
+
const u = usage;
|
|
288
|
+
// Anthropic: input_tokens/output_tokens; OpenAI: prompt_tokens/completion_tokens
|
|
289
|
+
inputTokens =
|
|
290
|
+
typeof u['input_tokens'] === 'number'
|
|
291
|
+
? u['input_tokens']
|
|
292
|
+
: typeof u['prompt_tokens'] === 'number'
|
|
293
|
+
? u['prompt_tokens']
|
|
294
|
+
: 0;
|
|
295
|
+
outputTokens =
|
|
296
|
+
typeof u['output_tokens'] === 'number'
|
|
297
|
+
? u['output_tokens']
|
|
298
|
+
: typeof u['completion_tokens'] === 'number'
|
|
299
|
+
? u['completion_tokens']
|
|
300
|
+
: 0;
|
|
301
|
+
}
|
|
302
|
+
bus.push(sessionId, {
|
|
303
|
+
kind: 'message_end',
|
|
304
|
+
stopReason,
|
|
305
|
+
inputTokens,
|
|
306
|
+
outputTokens,
|
|
307
|
+
timestamp: now,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
55
310
|
export function createMitmProxy(options) {
|
|
311
|
+
// Token stream extractor installation is gated on `tokenSessionId`.
|
|
312
|
+
// This is a mutable per-proxy value that callers flip around each
|
|
313
|
+
// active agent (see `setTokenSessionId` on the returned handle). When
|
|
314
|
+
// `undefined`, all extractor-install sites skip `bus.push()` entirely
|
|
315
|
+
// — no sentinel value is published. The module-scoped singleton bus is
|
|
316
|
+
// fetched lazily at each push site so `resetTokenStreamBus()` between
|
|
317
|
+
// tests is honored.
|
|
318
|
+
let tokenSessionId = options.initialTokenSessionId;
|
|
319
|
+
/**
|
|
320
|
+
* Capture-attribution session ID. Decoupled from `tokenSessionId` so
|
|
321
|
+
* a future extractor-related flip cannot reattribute in-flight
|
|
322
|
+
* captured exchanges. See §11 in the capture design.
|
|
323
|
+
*/
|
|
324
|
+
let captureSessionId;
|
|
325
|
+
/** Persona stamped onto subsequent captured ExchangeRecords. */
|
|
326
|
+
let capturePersona;
|
|
327
|
+
const agentKind = options.agentKind;
|
|
328
|
+
const captureWriter = options.capture;
|
|
329
|
+
const captureRecordedAgentName = options.recordedAgentName;
|
|
330
|
+
const captureWorkflowRunId = options.workflowRunId;
|
|
331
|
+
const captureBundleId = options.bundleId;
|
|
56
332
|
// Parse CA cert and key from PEM
|
|
57
333
|
const caCert = forge.pki.certificateFromPem(options.ca.certPem);
|
|
58
334
|
const caKey = forge.pki.privateKeyFromPem(options.ca.keyPem);
|
|
335
|
+
// Hostnames are case-insensitive (RFC 1035 §2.3.3, RFC 7230 §5.4). Normalize
|
|
336
|
+
// to lowercase at every storage and lookup site so the allowlist cannot be
|
|
337
|
+
// bypassed by varying the case (e.g. `CONNECT API.TEST.COM:443` while the
|
|
338
|
+
// provider is registered as `api.test.com`).
|
|
339
|
+
function normalizeHost(hostname) {
|
|
340
|
+
return hostname.toLowerCase();
|
|
341
|
+
}
|
|
59
342
|
// Build host → provider lookup
|
|
60
343
|
const providersByHost = new Map();
|
|
61
344
|
for (const mapping of options.providers) {
|
|
62
|
-
providersByHost.set(mapping.config.host, mapping);
|
|
345
|
+
providersByHost.set(normalizeHost(mapping.config.host), mapping);
|
|
63
346
|
}
|
|
64
347
|
// Dynamically added passthrough hosts (no key swap, no endpoint filtering)
|
|
65
348
|
const passthroughHosts = new Set();
|
|
349
|
+
// Escape hatch: when IRONCURTAIN_MITM_ALLOW_ALL_HOSTS is set to "1" or "true",
|
|
350
|
+
// any host that is NOT a configured provider or registry is treated as a
|
|
351
|
+
// passthrough tunnel. Known providers/registries still flow through the
|
|
352
|
+
// normal MITM path (TLS termination, credential swap, endpoint filtering) —
|
|
353
|
+
// this flag only widens the catch-all for unknown destinations. Defeats
|
|
354
|
+
// egress filtering for unknown hosts; only use in trusted environments.
|
|
355
|
+
const allowAllHosts = process.env.IRONCURTAIN_MITM_ALLOW_ALL_HOSTS === '1' || process.env.IRONCURTAIN_MITM_ALLOW_ALL_HOSTS === 'true';
|
|
66
356
|
// Certificate cache: hostname → { ctx, expiresAt }
|
|
67
357
|
const certCache = new Map();
|
|
68
358
|
// Renew leaf certs 1 hour before they expire
|
|
@@ -104,14 +394,23 @@ export function createMitmProxy(options) {
|
|
|
104
394
|
const registriesByHost = new Map();
|
|
105
395
|
if (options.registries) {
|
|
106
396
|
for (const reg of options.registries) {
|
|
107
|
-
registriesByHost.set(reg.host, reg);
|
|
397
|
+
registriesByHost.set(normalizeHost(reg.host), reg);
|
|
108
398
|
if (reg.mirrorHosts) {
|
|
109
399
|
for (const mirror of reg.mirrorHosts) {
|
|
110
|
-
registriesByHost.set(mirror, reg);
|
|
400
|
+
registriesByHost.set(normalizeHost(mirror), reg);
|
|
111
401
|
}
|
|
112
402
|
}
|
|
113
403
|
}
|
|
114
404
|
}
|
|
405
|
+
// True when a host is statically known to the proxy. Gates the wildcard
|
|
406
|
+
// escape hatch so known providers/registries always follow their dedicated
|
|
407
|
+
// MITM/registry path, never the passthrough tunnel. Accepts the hostname
|
|
408
|
+
// as given (case may vary on the wire); normalization happens here so all
|
|
409
|
+
// callers can trust the result regardless of input case.
|
|
410
|
+
function isKnownStaticHost(hostname) {
|
|
411
|
+
const h = normalizeHost(hostname);
|
|
412
|
+
return providersByHost.has(h) || registriesByHost.has(h);
|
|
413
|
+
}
|
|
115
414
|
// AllowedVersionCache for tarball backstop
|
|
116
415
|
const allowedVersionCache = new Map();
|
|
117
416
|
const activeClientSockets = new Set();
|
|
@@ -130,6 +429,42 @@ export function createMitmProxy(options) {
|
|
|
130
429
|
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
131
430
|
}
|
|
132
431
|
});
|
|
432
|
+
// Fast-reject WebSocket upgrades on TLS-intercepted provider hosts.
|
|
433
|
+
//
|
|
434
|
+
// Codex CLI ≥ 0.142 talks to its inference endpoint over
|
|
435
|
+
// `wss://chatgpt.com/backend-api/codex/responses` (HTTP `GET` with
|
|
436
|
+
// `Upgrade: websocket`) and only falls back to the legacy `POST` + SSE
|
|
437
|
+
// path when the WS connection fails at the TRANSPORT level. Without this
|
|
438
|
+
// listener Node delivers the upgrade request to the `'request'` handler
|
|
439
|
+
// below, which forwards it via `https.request()`; the upstream's `101
|
|
440
|
+
// Switching Protocols` is then dropped on the floor (Node fires only
|
|
441
|
+
// `'close'` on a client request with no `'upgrade'` listener), and the
|
|
442
|
+
// agent's WS client sits on an open socket until its ~15s connect
|
|
443
|
+
// timeout, retrying ~7× (~112s) before falling back.
|
|
444
|
+
//
|
|
445
|
+
// We deliberately DESTROY the socket rather than replying with an HTTP
|
|
446
|
+
// 4xx/5xx: tungstenite surfaces a non-101 HTTP response as a fatal
|
|
447
|
+
// handshake error to the Codex turn (observed: `turn.failed` with the
|
|
448
|
+
// body text), whereas a connection-level failure (reset / EOF before
|
|
449
|
+
// handshake) is treated as a retryable transport error and triggers the
|
|
450
|
+
// POST/SSE fallback — the same path the timeout used to reach, just in
|
|
451
|
+
// milliseconds instead of ~15s per attempt.
|
|
452
|
+
//
|
|
453
|
+
// This is intentionally a fast-fail, NOT a bridge: bridging WS for
|
|
454
|
+
// provider hosts would let inference traffic bypass the SSE trajectory
|
|
455
|
+
// capture / token-stream extraction paths. A proper key-swapping WS
|
|
456
|
+
// bridge (with WS-frame reassembly for capture) is a follow-up; until
|
|
457
|
+
// then the agent's HTTP fallback is the supported transport.
|
|
458
|
+
innerServer.on('upgrade', (req, socket) => {
|
|
459
|
+
const meta = socketMetadata.get(req.socket);
|
|
460
|
+
const targetHost = meta?.host ?? 'unknown';
|
|
461
|
+
logger.info(`[mitm-proxy] REJECTED ${req.headers.upgrade ?? 'upgrade'} ${targetHost}${req.url ?? ''} ` +
|
|
462
|
+
'(WebSocket not bridged for MITM provider hosts; agent should fall back to HTTP)');
|
|
463
|
+
socket.on('error', () => {
|
|
464
|
+
/* swallow — socket is being torn down */
|
|
465
|
+
});
|
|
466
|
+
socket.destroy();
|
|
467
|
+
});
|
|
133
468
|
// Lazy-loaded registry handler to avoid circular imports.
|
|
134
469
|
// Cached after first import so subsequent calls are synchronous.
|
|
135
470
|
let registryHandlerModule;
|
|
@@ -193,6 +528,45 @@ export function createMitmProxy(options) {
|
|
|
193
528
|
}
|
|
194
529
|
const provider = meta.provider;
|
|
195
530
|
const { method, url: path, headers } = clientReq;
|
|
531
|
+
// Per-handler capture-attribution snapshot. Reading once at handler
|
|
532
|
+
// entry keeps every captured byte tied to the session active when
|
|
533
|
+
// the request arrived, even if `setCaptureSessionId(...)` flips
|
|
534
|
+
// mid-exchange. The snapshot is per-closure so HTTP keep-alive
|
|
535
|
+
// concurrent exchanges on the same TLS socket get independent
|
|
536
|
+
// attribution (§11 parallel-keep-alive correctness).
|
|
537
|
+
const sidAtCapture = captureSessionId;
|
|
538
|
+
let captureHandle;
|
|
539
|
+
// Capture-endpoint gate: only training-relevant completion endpoints are
|
|
540
|
+
// recorded. Host-shared housekeeping traffic (registry pagination,
|
|
541
|
+
// telemetry batches, settings/eval pings) is excluded so it never
|
|
542
|
+
// pollutes the corpus. Strictly subtractive — when capture is off or the
|
|
543
|
+
// endpoint isn't capturable, `captureHandle` stays undefined and
|
|
544
|
+
// `attachCaptureBranch` is a no-op; forwarding, key swap, and the
|
|
545
|
+
// 401-retry path are unaffected. The `captureWriter` short-circuit keeps
|
|
546
|
+
// a disabled proxy at zero per-request cost (no header clone, no glob
|
|
547
|
+
// match). See §3 integration point 4 of the capture design doc.
|
|
548
|
+
if (captureWriter && sidAtCapture && isCapturableEndpoint(provider.config, method, path)) {
|
|
549
|
+
// Snapshot the original (pre-key-swap) headers for the capture
|
|
550
|
+
// record. `modifiedHeaders` (below) gets the real key injected by
|
|
551
|
+
// `validateAndSwapApiKey` — we must capture from `headers`, not
|
|
552
|
+
// `modifiedHeaders`. The writer applies a second redaction pass
|
|
553
|
+
// defensively.
|
|
554
|
+
const captureRequestHeaders = { ...headers };
|
|
555
|
+
captureHandle = beginCaptureExchange({
|
|
556
|
+
writer: captureWriter,
|
|
557
|
+
sessionId: sidAtCapture,
|
|
558
|
+
persona: capturePersona,
|
|
559
|
+
workflowRunId: captureWorkflowRunId,
|
|
560
|
+
bundleId: captureBundleId,
|
|
561
|
+
recordedAgentName: captureRecordedAgentName,
|
|
562
|
+
host: targetHost,
|
|
563
|
+
path: String(path),
|
|
564
|
+
method: String(method),
|
|
565
|
+
requestHeaders: captureRequestHeaders,
|
|
566
|
+
requestContentEncoding: clientReq.headers['content-encoding']?.toLowerCase(),
|
|
567
|
+
requestStartedAt: Date.now(),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
196
570
|
// 1. Endpoint filtering
|
|
197
571
|
if (!isEndpointAllowed(provider.config, method, path)) {
|
|
198
572
|
logger.info(`[mitm-proxy] BLOCKED ${method} ${targetHost}${path}`);
|
|
@@ -219,11 +593,14 @@ export function createMitmProxy(options) {
|
|
|
219
593
|
const needsRewrite = shouldRewriteBody(provider.config, method, path);
|
|
220
594
|
// 401 retry only makes sense when we swapped our own managed credential
|
|
221
595
|
const canRetryAuth = keyResult.swapped && !!provider.tokenManager;
|
|
222
|
-
// Only buffer for retry when the body is small enough (known
|
|
223
|
-
// under 1MB). Large or chunked bodies stream through without
|
|
224
|
-
// to avoid memory overhead and 413 rejections on large
|
|
596
|
+
// Only buffer for retry when the body is absent or small enough (known
|
|
597
|
+
// Content-Length under 1MB). Large or chunked bodies stream through without
|
|
598
|
+
// retry support to avoid memory overhead and 413 rejections on large
|
|
599
|
+
// payloads. Bodyless requests (notably Codex's model-list GET) still need
|
|
600
|
+
// the OAuth 401 refresh path.
|
|
225
601
|
const contentLength = parseInt(clientReq.headers['content-length'] ?? '', 10);
|
|
226
|
-
const
|
|
602
|
+
const hasNoRequestBody = clientReq.headers['content-length'] === undefined && clientReq.headers['transfer-encoding'] === undefined;
|
|
603
|
+
const retryBufferOk = canRetryAuth && (hasNoRequestBody || (Number.isFinite(contentLength) && contentLength <= MAX_RETRY_BODY_BYTES));
|
|
227
604
|
const needsBuffer = needsRewrite || retryBufferOk;
|
|
228
605
|
/**
|
|
229
606
|
* Sends the request upstream with optional body override and 401 retry.
|
|
@@ -294,7 +671,140 @@ export function createMitmProxy(options) {
|
|
|
294
671
|
clientRes.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
|
|
295
672
|
clientRes.flushHeaders();
|
|
296
673
|
clientRes.socket?.setNoDelay(true);
|
|
297
|
-
upstreamRes.
|
|
674
|
+
const contentType = upstreamRes.headers['content-type'] ?? '';
|
|
675
|
+
// Pin this response's events to the id active when it started,
|
|
676
|
+
// so an orchestrator mid-flight flip can't split one stream.
|
|
677
|
+
const sidAtAttach = tokenSessionId;
|
|
678
|
+
// Trajectory-capture response branch. The 401 refresh-retry
|
|
679
|
+
// path above already returned before reaching here, so the
|
|
680
|
+
// captureTap is bound only to the SUCCESSFUL upstream
|
|
681
|
+
// response — never the drained 401 (§5 "401 refresh-retry
|
|
682
|
+
// semantics").
|
|
683
|
+
//
|
|
684
|
+
// The capture branch is FANNED OUT from the upstream response
|
|
685
|
+
// (it does NOT sit in-series on the forwarding pipeline).
|
|
686
|
+
// Anthropic serves `/v1/messages` SSE with `content-encoding:
|
|
687
|
+
// gzip` by default; the forwarding path must continue to
|
|
688
|
+
// deliver the raw compressed bytes to the agent (its SDK
|
|
689
|
+
// decompresses transparently), while the capture path needs
|
|
690
|
+
// decompressed bytes so the reassembler can parse SSE events.
|
|
691
|
+
// See §3 and §6 invariant 6 of the design doc.
|
|
692
|
+
attachCaptureBranch(upstreamRes, captureHandle, sidAtCapture);
|
|
693
|
+
if (sidAtAttach && contentType.includes('text/event-stream')) {
|
|
694
|
+
const tokenBus = getTokenStreamBus();
|
|
695
|
+
const sseProvider = resolveSseProvider(targetHost);
|
|
696
|
+
const extractor = new SseExtractorTransform(sseProvider, (event) => {
|
|
697
|
+
tokenBus.push(sidAtAttach, event);
|
|
698
|
+
});
|
|
699
|
+
upstreamRes.pipe(extractor).pipe(clientRes);
|
|
700
|
+
}
|
|
701
|
+
else if (sidAtAttach && isLlmMessagesEndpoint(path) && contentType.includes('application/json')) {
|
|
702
|
+
// Non-streaming JSON response: buffer for extraction while piping to client.
|
|
703
|
+
// The passthrough stream always forwards the full response to the client.
|
|
704
|
+
// Capture is bounded at MAX_JSON_RESPONSE_CAPTURE_BYTES: once exceeded,
|
|
705
|
+
// we stop accumulating chunks (and skip extraction at end-of-stream),
|
|
706
|
+
// but the passthrough listener stays attached so the stream completes
|
|
707
|
+
// normally and the client still sees every byte.
|
|
708
|
+
const passthrough = new PassThrough();
|
|
709
|
+
const capture = createBoundedJsonResponseCapture();
|
|
710
|
+
passthrough.on('data', (chunk) => capture.onData(chunk));
|
|
711
|
+
passthrough.on('end', () => {
|
|
712
|
+
capture.onEnd((body) => {
|
|
713
|
+
if (body === null)
|
|
714
|
+
return; // overflowed -- skip extraction
|
|
715
|
+
try {
|
|
716
|
+
extractFromJsonResponse(body, sidAtAttach);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// Extraction errors must never affect the forwarding path
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
upstreamRes.pipe(passthrough).pipe(clientRes);
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
upstreamRes.pipe(clientRes);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Attach the trajectory capture branch to `upstreamRes` via a
|
|
730
|
+
* fan-out tee. Bytes the agent receives are unaffected. When
|
|
731
|
+
* `content-encoding` is supported we decompress before feeding
|
|
732
|
+
* the captureTap; unsupported encodings poison the session
|
|
733
|
+
* with `unsupported-encoding`.
|
|
734
|
+
*/
|
|
735
|
+
function attachCaptureBranch(upstream, handle, sessionId) {
|
|
736
|
+
if (!handle || !sessionId)
|
|
737
|
+
return;
|
|
738
|
+
const captureTap = handle.attachResponse({
|
|
739
|
+
statusCode: upstream.statusCode ?? 502,
|
|
740
|
+
headers: upstream.headers,
|
|
741
|
+
});
|
|
742
|
+
const inlet = createResponseCaptureInlet({
|
|
743
|
+
captureTap,
|
|
744
|
+
contentEncoding: upstream.headers['content-encoding'],
|
|
745
|
+
captureHandle: handle,
|
|
746
|
+
onPoison: (reason) => {
|
|
747
|
+
try {
|
|
748
|
+
captureWriter?.markSessionPoisoned(sessionId, reason);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
/* swallow — capture must never disturb forwarding */
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
// Tee upstream bytes into the capture inlet via data
|
|
756
|
+
// listeners. This pattern coexists with the sibling
|
|
757
|
+
// `upstreamRes.pipe(...)` for forwarding because Node
|
|
758
|
+
// broadcasts `data` events to all listeners, and pipe is
|
|
759
|
+
// implemented on top of the same event. Errors / aborts on
|
|
760
|
+
// the upstream stream are propagated to the inlet so the
|
|
761
|
+
// captureTap can settle cleanly (or surface a poison).
|
|
762
|
+
upstream.on('data', (chunk) => {
|
|
763
|
+
if (!inlet.writableEnded && !inlet.destroyed) {
|
|
764
|
+
inlet.write(chunk);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
upstream.on('end', () => {
|
|
768
|
+
if (!inlet.writableEnded && !inlet.destroyed) {
|
|
769
|
+
inlet.end();
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
upstream.on('error', () => {
|
|
773
|
+
// On upstream 'error'/'aborted' a terminal event may already
|
|
774
|
+
// be buffered in a decompressor inlet. Prefer a graceful
|
|
775
|
+
// `inlet.end()` flush (NOT destroy) so zlib `_flush` emits the
|
|
776
|
+
// buffered tail and the tap's `canFinalize()` can recover a
|
|
777
|
+
// complete-but-aborted stream; `canFinalize()` is the primary
|
|
778
|
+
// recovery mechanism (codex is uncompressed, so the inlet is
|
|
779
|
+
// the captureTap itself and `end()` simply triggers finalize).
|
|
780
|
+
// Genuine decompressor corruption surfaces as a zlib 'error'
|
|
781
|
+
// on the inlet, which poisons `reassembly-failure` via
|
|
782
|
+
// `createResponseCaptureInlet`'s own handler. Reserve
|
|
783
|
+
// `inlet.destroy()` for the case where the inlet cannot be
|
|
784
|
+
// ended (already errored).
|
|
785
|
+
if (!inlet.writableEnded && !inlet.destroyed) {
|
|
786
|
+
inlet.end();
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
upstream.on('close', () => {
|
|
790
|
+
// If `end` already fired, the inlet has been ended. If we
|
|
791
|
+
// got here without `end` and without an `error`, this is a
|
|
792
|
+
// graceful close-after-data (e.g. the client disconnected
|
|
793
|
+
// after the completion). Call `inlet.end()` — NOT
|
|
794
|
+
// `inlet.destroy()` — so a decompressor inlet runs zlib
|
|
795
|
+
// `_flush`, emits its buffered tail + `end` to the
|
|
796
|
+
// captureTap, and the reassembler sees its terminal event.
|
|
797
|
+
// `inlet.destroy(err)` is reserved for genuine upstream
|
|
798
|
+
// `error` (handled above), which would discard the
|
|
799
|
+
// decompressor's pending tail. The tap's `canFinalize()`
|
|
800
|
+
// lifecycle then writes a faithful record for a
|
|
801
|
+
// complete-but-socket-aborted stream, or poisons
|
|
802
|
+
// mid-stream-abort only if the terminal event never landed.
|
|
803
|
+
if (!inlet.writableEnded && !inlet.destroyed) {
|
|
804
|
+
inlet.end();
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
298
808
|
});
|
|
299
809
|
activeUpstreamRequests.add(upstreamReq);
|
|
300
810
|
upstreamReq.on('close', () => activeUpstreamRequests.delete(upstreamReq));
|
|
@@ -324,7 +834,23 @@ export function createMitmProxy(options) {
|
|
|
324
834
|
log(`[mitm-proxy] client request error: ${err.message}`);
|
|
325
835
|
upstreamReq.destroy();
|
|
326
836
|
});
|
|
327
|
-
|
|
837
|
+
// Trajectory-capture tee: when capture is active and we are NOT
|
|
838
|
+
// buffering for rewrite/retry, install a PassThrough between
|
|
839
|
+
// clientReq and upstreamReq so we can observe each request chunk
|
|
840
|
+
// on the way through. The tap's data listener never modifies
|
|
841
|
+
// bytes; the forwarding pipe stays line-rate. Adding a 'data'
|
|
842
|
+
// listener directly to clientReq would flip it to flowing mode
|
|
843
|
+
// and race the upstream pipe — the PassThrough avoids that.
|
|
844
|
+
if (captureHandle) {
|
|
845
|
+
const reqTap = new PassThrough();
|
|
846
|
+
const handle = captureHandle;
|
|
847
|
+
reqTap.on('data', (chunk) => handle.pushRequestChunk(chunk));
|
|
848
|
+
reqTap.on('end', () => handle.finishRequest());
|
|
849
|
+
clientReq.pipe(reqTap).pipe(upstreamReq);
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
clientReq.pipe(upstreamReq);
|
|
853
|
+
}
|
|
328
854
|
}
|
|
329
855
|
}
|
|
330
856
|
/**
|
|
@@ -370,17 +896,34 @@ export function createMitmProxy(options) {
|
|
|
370
896
|
}
|
|
371
897
|
return;
|
|
372
898
|
}
|
|
899
|
+
// Trajectory-capture: feed the buffered raw body into the
|
|
900
|
+
// capture record. This is the pre-rewrite, pre-key-swap body —
|
|
901
|
+
// exactly what the agent emitted. See §3 / §6 invariant #2.
|
|
902
|
+
captureHandle?.setRequestBody(rawBody);
|
|
373
903
|
let finalBody = rawBody;
|
|
904
|
+
// Extract tool_result events from the request body for token stream observation.
|
|
905
|
+
// Snapshot here (before rewrite) so a concurrent setTokenSessionId
|
|
906
|
+
// flip can't split this request's events across two ids.
|
|
907
|
+
const sidForToolResults = tokenSessionId;
|
|
908
|
+
if (sidForToolResults && isLlmMessagesEndpoint(path)) {
|
|
909
|
+
try {
|
|
910
|
+
const bodyParsed = JSON.parse(rawBody.toString());
|
|
911
|
+
extractToolResults(bodyParsed, sidForToolResults);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
// Parse failure is fine -- the rewriter below will also attempt to parse
|
|
915
|
+
}
|
|
916
|
+
}
|
|
374
917
|
if (needsRewrite) {
|
|
375
918
|
const rewriter = provider.config.requestRewriter;
|
|
376
919
|
const reqMethod = method;
|
|
377
920
|
const reqPath = path;
|
|
378
921
|
try {
|
|
379
922
|
const parsed = JSON.parse(rawBody.toString());
|
|
380
|
-
const result = rewriter(parsed, { method: reqMethod, path: reqPath });
|
|
923
|
+
const result = rewriter(parsed, { method: reqMethod, path: reqPath, agentKind });
|
|
381
924
|
if (result) {
|
|
382
925
|
finalBody = Buffer.from(JSON.stringify(result.modified));
|
|
383
|
-
logger.info(`[mitm-proxy] POST ${targetHost}${path} - stripped
|
|
926
|
+
logger.info(`[mitm-proxy] POST ${targetHost}${path} - stripped tools: ${result.stripped.join(', ')}`);
|
|
384
927
|
}
|
|
385
928
|
}
|
|
386
929
|
catch {
|
|
@@ -505,11 +1048,29 @@ export function createMitmProxy(options) {
|
|
|
505
1048
|
clientReq.pipe(upstreamReq);
|
|
506
1049
|
}
|
|
507
1050
|
// Outer server - UDS listener, handles CONNECT and plain HTTP proxy requests
|
|
508
|
-
const outerServer = http.createServer(
|
|
509
|
-
|
|
1051
|
+
const outerServer = http.createServer();
|
|
1052
|
+
// Connection-source guard (tcp-hostonly topology): destroy sockets from
|
|
1053
|
+
// outside the allowed subnet before any HTTP parsing happens. Covers
|
|
1054
|
+
// plain requests, CONNECT, and WebSocket upgrades alike since all ride
|
|
1055
|
+
// on the same TCP connections.
|
|
1056
|
+
if (options.allowRemoteAddress) {
|
|
1057
|
+
const allowRemoteAddress = options.allowRemoteAddress;
|
|
1058
|
+
outerServer.on('connection', (socket) => {
|
|
1059
|
+
if (!allowRemoteAddress(socket.remoteAddress)) {
|
|
1060
|
+
socket.destroy();
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
outerServer.on('request', (req, res) => {
|
|
1065
|
+
// Handle plain HTTP proxy requests for passthrough and Debian registry domains.
|
|
510
1066
|
// HTTP proxy clients send absolute URLs: "GET http://host:port/path".
|
|
1067
|
+
// Only Debian registries are included here because apt uses plain HTTP
|
|
1068
|
+
// by default. npm/PyPI use HTTPS CONNECT and go through the registry-proxy
|
|
1069
|
+
// validation path instead.
|
|
511
1070
|
const parsed = req.url ? tryParseProxyUrl(req.url) : null;
|
|
512
|
-
|
|
1071
|
+
const isDebianRegistry = parsed ? registriesByHost.get(parsed.hostname)?.type === 'debian' : false;
|
|
1072
|
+
const isWildcardEligible = !!parsed && allowAllHosts && !isKnownStaticHost(parsed.hostname);
|
|
1073
|
+
if (parsed && (passthroughHosts.has(parsed.hostname) || isDebianRegistry || isWildcardEligible)) {
|
|
513
1074
|
forwardPlainHttpPassthrough(req, res, parsed.hostname, parsed.port, parsed.path);
|
|
514
1075
|
return;
|
|
515
1076
|
}
|
|
@@ -526,7 +1087,12 @@ export function createMitmProxy(options) {
|
|
|
526
1087
|
outerServer.on('connect', (req, clientSocket, head) => {
|
|
527
1088
|
const url = req.url ?? '';
|
|
528
1089
|
const colonIndex = url.lastIndexOf(':');
|
|
529
|
-
const
|
|
1090
|
+
const rawHost = colonIndex > 0 ? url.substring(0, colonIndex) : url;
|
|
1091
|
+
// Normalize so the allowlist is case-insensitive (RFC 1035 §2.3.3).
|
|
1092
|
+
// Critical when the wildcard escape hatch is enabled — without this,
|
|
1093
|
+
// `CONNECT API.TEST.COM:443` would miss the provider map and be treated
|
|
1094
|
+
// as an unknown host, opening a raw tunnel that bypasses MITM.
|
|
1095
|
+
const host = normalizeHost(rawHost);
|
|
530
1096
|
const port = colonIndex > 0 ? parseInt(url.substring(colonIndex + 1), 10) : 443;
|
|
531
1097
|
const connId = ++connectionId;
|
|
532
1098
|
// Handle client socket errors early to prevent uncaught 'error' events
|
|
@@ -541,14 +1107,17 @@ export function createMitmProxy(options) {
|
|
|
541
1107
|
// 1. Check allowlist (providers, registries, and dynamic passthrough)
|
|
542
1108
|
const provider = providersByHost.get(host);
|
|
543
1109
|
const registry = registriesByHost.get(host);
|
|
544
|
-
const
|
|
1110
|
+
const isDynamicPassthrough = passthroughHosts.has(host);
|
|
1111
|
+
const isWildcardEligible = allowAllHosts && !isKnownStaticHost(host);
|
|
1112
|
+
const isPassthrough = !provider && !registry && (isDynamicPassthrough || isWildcardEligible);
|
|
545
1113
|
if (!provider && !registry && !isPassthrough) {
|
|
546
1114
|
logger.info(`[mitm-proxy] #${connId} DENIED CONNECT ${host}:${port}`);
|
|
547
1115
|
clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
548
1116
|
clientSocket.destroy();
|
|
549
1117
|
return;
|
|
550
1118
|
}
|
|
551
|
-
const
|
|
1119
|
+
const passthroughKind = isDynamicPassthrough ? 'passthrough' : 'passthrough-wildcard';
|
|
1120
|
+
const connType = provider ? 'provider' : registry ? 'registry' : passthroughKind;
|
|
552
1121
|
// 2. For passthrough domains, create a raw TCP tunnel (no MITM).
|
|
553
1122
|
// The proxy just pipes bytes bidirectionally — this supports both
|
|
554
1123
|
// plain HTTP and WebSocket connections through CONNECT.
|
|
@@ -644,7 +1213,8 @@ export function createMitmProxy(options) {
|
|
|
644
1213
|
// of routing through the normal 'request' handler.
|
|
645
1214
|
outerServer.on('upgrade', (req, clientSocket, head) => {
|
|
646
1215
|
const parsed = req.url ? tryParseProxyUrl(req.url) : null;
|
|
647
|
-
|
|
1216
|
+
const isWildcardEligible = !!parsed && allowAllHosts && !isKnownStaticHost(parsed.hostname);
|
|
1217
|
+
if (!parsed || (!passthroughHosts.has(parsed.hostname) && !isWildcardEligible)) {
|
|
648
1218
|
clientSocket.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
|
|
649
1219
|
return;
|
|
650
1220
|
}
|
|
@@ -824,17 +1394,18 @@ export function createMitmProxy(options) {
|
|
|
824
1394
|
const hostController = {
|
|
825
1395
|
addHost(domain) {
|
|
826
1396
|
validateDomain(domain);
|
|
827
|
-
|
|
1397
|
+
const h = normalizeHost(domain);
|
|
1398
|
+
if (providersByHost.has(h))
|
|
828
1399
|
return false;
|
|
829
|
-
if (passthroughHosts.has(
|
|
1400
|
+
if (passthroughHosts.has(h))
|
|
830
1401
|
return false;
|
|
831
|
-
passthroughHosts.add(
|
|
1402
|
+
passthroughHosts.add(h);
|
|
832
1403
|
// Pre-warm cert cache for the new domain
|
|
833
|
-
getOrCreateSecureContext(
|
|
1404
|
+
getOrCreateSecureContext(h);
|
|
834
1405
|
return true;
|
|
835
1406
|
},
|
|
836
1407
|
removeHost(domain) {
|
|
837
|
-
return passthroughHosts.delete(domain);
|
|
1408
|
+
return passthroughHosts.delete(normalizeHost(domain));
|
|
838
1409
|
},
|
|
839
1410
|
listHosts() {
|
|
840
1411
|
return {
|
|
@@ -936,10 +1507,25 @@ export function createMitmProxy(options) {
|
|
|
936
1507
|
}
|
|
937
1508
|
return {
|
|
938
1509
|
hosts: hostController,
|
|
1510
|
+
setTokenSessionId(id) {
|
|
1511
|
+
tokenSessionId = id;
|
|
1512
|
+
},
|
|
1513
|
+
setCaptureSessionId(id) {
|
|
1514
|
+
captureSessionId = id;
|
|
1515
|
+
},
|
|
1516
|
+
setCapturePersona(persona) {
|
|
1517
|
+
capturePersona = persona;
|
|
1518
|
+
},
|
|
939
1519
|
async start() {
|
|
1520
|
+
// Emit the egress-filter downgrade notice once per actual listening
|
|
1521
|
+
// proxy. Construction-time logging would fire even for proxies that
|
|
1522
|
+
// are created and discarded (tests, aborted setup).
|
|
1523
|
+
if (allowAllHosts) {
|
|
1524
|
+
logger.warn('[mitm-proxy] IRONCURTAIN_MITM_ALLOW_ALL_HOSTS is set — unknown hosts will be tunneled (egress filtering DISABLED for unknown hosts)');
|
|
1525
|
+
}
|
|
940
1526
|
// Pre-warm cert cache for all configured providers and registries
|
|
941
1527
|
for (const mapping of options.providers) {
|
|
942
|
-
getOrCreateSecureContext(mapping.config.host);
|
|
1528
|
+
getOrCreateSecureContext(normalizeHost(mapping.config.host));
|
|
943
1529
|
}
|
|
944
1530
|
for (const host of registriesByHost.keys()) {
|
|
945
1531
|
getOrCreateSecureContext(host);
|