@provos/ironcurtain 0.11.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 +9 -7
- 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 +8 -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 +116 -1
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/first-start.js +2 -8
- package/dist/config/first-start.js.map +1 -1
- 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 +48 -6
- package/dist/config/paths.js +63 -8
- package/dist/config/paths.js.map +1 -1
- package/dist/config/user-config.d.ts +48 -1
- package/dist/config/user-config.js +49 -1
- package/dist/config/user-config.js.map +1 -1
- package/dist/daemon/daemon-command.js +14 -5
- package/dist/daemon/daemon-command.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.d.ts +18 -0
- package/dist/daemon/ironcurtain-daemon.js +22 -1
- 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/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/agent-adapter.d.ts +5 -1
- package/dist/docker/agent-adapter.js +2 -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 +11 -0
- package/dist/docker/code-mode-proxy.js +3 -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.js +30 -1
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +186 -9
- package/dist/docker/docker-infrastructure.js +521 -77
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.d.ts +25 -5
- package/dist/docker/docker-manager.js +262 -22
- 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 +1 -1
- package/dist/docker/mitm-proxy.d.ts +52 -1
- package/dist/docker/mitm-proxy.js +234 -10
- 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 +13 -0
- package/dist/docker/oauth-credentials.js +125 -0
- package/dist/docker/oauth-credentials.js.map +1 -1
- package/dist/docker/package-types.d.ts +1 -1
- package/dist/docker/provider-config.d.ts +29 -1
- package/dist/docker/provider-config.js +93 -3
- 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/pty-session.d.ts +15 -0
- package/dist/docker/pty-session.js +139 -67
- package/dist/docker/pty-session.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 +14 -5
- package/dist/docker/resource-limits.js +11 -8
- package/dist/docker/resource-limits.js.map +1 -1
- 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 +53 -7
- package/dist/doctor/checks.d.ts +36 -3
- package/dist/doctor/checks.js +102 -15
- package/dist/doctor/checks.js.map +1 -1
- package/dist/doctor/doctor-command.js +39 -9
- package/dist/doctor/doctor-command.js.map +1 -1
- package/dist/doctor/mcp-liveness.d.ts +1 -1
- package/dist/doctor/mcp-liveness.js +12 -4
- package/dist/doctor/mcp-liveness.js.map +1 -1
- package/dist/index.js +30 -12
- package/dist/index.js.map +1 -1
- 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.js +19 -6
- package/dist/mux/mux-command.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/observe/observe-command.js +112 -125
- package/dist/observe/observe-command.js.map +1 -1
- 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/persona-command.js +46 -76
- 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/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 +12 -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/pipeline-runner.d.ts +63 -4
- package/dist/pipeline/pipeline-runner.js +159 -50
- package/dist/pipeline/pipeline-runner.js.map +1 -1
- package/dist/pipeline/pipeline-shared.d.ts +32 -3
- package/dist/pipeline/pipeline-shared.js +76 -8
- package/dist/pipeline/pipeline-shared.js.map +1 -1
- package/dist/pipeline/refresh-lists.js +3 -5
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/sandbox/index.d.ts +19 -4
- package/dist/sandbox/index.js +31 -18
- package/dist/sandbox/index.js.map +1 -1
- package/dist/session/index.js +10 -1
- package/dist/session/index.js.map +1 -1
- package/dist/session/preflight.d.ts +1 -30
- package/dist/session/preflight.js +52 -85
- package/dist/session/preflight.js.map +1 -1
- package/dist/session/types.d.ts +9 -0
- package/dist/signal/bot-command.js +6 -1
- package/dist/signal/bot-command.js.map +1 -1
- package/dist/signal/setup-signal.js +1 -1
- package/dist/signal/setup-signal.js.map +1 -1
- package/dist/signal/signal-container.d.ts +3 -3
- package/dist/signal/signal-container.js +2 -2
- package/dist/signal/signal-container.js.map +1 -1
- package/dist/trusted-process/call-circuit-breaker.d.ts +6 -0
- package/dist/trusted-process/call-circuit-breaker.js +22 -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/mcp-proxy-server.js +35 -3
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.js +13 -2
- package/dist/trusted-process/policy-engine.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 +22 -8
- package/dist/trusted-process/tool-call-coordinator.js +149 -65
- package/dist/trusted-process/tool-call-coordinator.js.map +1 -1
- package/dist/trusted-process/tool-call-pipeline.d.ts +8 -0
- package/dist/trusted-process/tool-call-pipeline.js +68 -34
- package/dist/trusted-process/tool-call-pipeline.js.map +1 -1
- 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/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/persona-dispatch.d.ts +46 -2
- package/dist/web-ui/dispatch/persona-dispatch.js +209 -56
- package/dist/web-ui/dispatch/persona-dispatch.js.map +1 -1
- package/dist/web-ui/dispatch/session-dispatch.js +22 -4
- package/dist/web-ui/dispatch/session-dispatch.js.map +1 -1
- package/dist/web-ui/dispatch/types.d.ts +23 -0
- package/dist/web-ui/dispatch/types.js +4 -0
- package/dist/web-ui/dispatch/types.js.map +1 -1
- package/dist/web-ui/dispatch/workflow-dispatch.js +116 -5
- package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -1
- package/dist/web-ui/json-rpc-dispatch.js +1 -1
- package/dist/web-ui/json-rpc-dispatch.js.map +1 -1
- package/dist/web-ui/web-event-bus.d.ts +25 -1
- package/dist/web-ui/web-event-bus.js.map +1 -1
- package/dist/web-ui/web-ui-server.d.ts +14 -0
- package/dist/web-ui/web-ui-server.js +16 -0
- package/dist/web-ui/web-ui-server.js.map +1 -1
- package/dist/web-ui/web-ui-types.d.ts +133 -5
- package/dist/web-ui/web-ui-types.js.map +1 -1
- 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/index.html +2 -2
- 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.js +0 -2
- package/dist/workflow/cli-support.js.map +1 -1
- 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/discovery.d.ts +29 -0
- package/dist/workflow/discovery.js +67 -7
- package/dist/workflow/discovery.js.map +1 -1
- 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.d.ts +7 -1
- package/dist/workflow/lint.js +186 -3
- package/dist/workflow/lint.js.map +1 -1
- package/dist/workflow/machine-builder.d.ts +90 -2
- package/dist/workflow/machine-builder.js +222 -29
- package/dist/workflow/machine-builder.js.map +1 -1
- package/dist/workflow/message-log.d.ts +15 -1
- package/dist/workflow/message-log.js.map +1 -1
- package/dist/workflow/orchestrator.d.ts +505 -1
- package/dist/workflow/orchestrator.js +1722 -69
- package/dist/workflow/orchestrator.js.map +1 -1
- package/dist/workflow/prompt-builder.js +2 -1
- package/dist/workflow/prompt-builder.js.map +1 -1
- package/dist/workflow/run-state-command.js +1 -1
- package/dist/workflow/run-state-command.js.map +1 -1
- package/dist/workflow/status-parser.js +19 -4
- package/dist/workflow/status-parser.js.map +1 -1
- package/dist/workflow/types.d.ts +123 -23
- package/dist/workflow/types.js +2 -0
- package/dist/workflow/types.js.map +1 -1
- package/dist/workflow/validate.d.ts +23 -1
- package/dist/workflow/validate.js +311 -22
- package/dist/workflow/validate.js.map +1 -1
- package/dist/workflow/workflow-command.d.ts +0 -17
- package/dist/workflow/workflow-command.js +110 -88
- package/dist/workflow/workflow-command.js.map +1 -1
- package/dist/workflow/workflow-discovery.d.ts +2 -0
- package/dist/workflow/workflow-discovery.js +22 -1
- package/dist/workflow/workflow-discovery.js.map +1 -1
- package/dist/workflow/workflow-manager.d.ts +34 -0
- package/dist/workflow/workflow-manager.js +52 -1
- package/dist/workflow/workflow-manager.js.map +1 -1
- package/dist/workflow/workflows/design-and-code/README.md +50 -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/workflow.yaml +1 -0
- package/dist/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
- package/dist/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
- package/dist/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
- package/dist/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
- package/docker/Dockerfile.base +22 -0
- package/docker/Dockerfile.base.arm64 +18 -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 +6 -45
- package/docker/entrypoint-codex.sh +42 -0
- package/docker/entrypoint-goose.sh +5 -24
- package/docker/entrypoint-uid-remap.sh +93 -0
- package/package.json +9 -4
- package/src/workflow/workflows/design-and-code/README.md +50 -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/workflow.yaml +1 -0
- package/src/workflow/workflows/vuln-discovery/skills/harness-design-fuzzing/SKILL.md +6 -1
- package/src/workflow/workflows/vuln-discovery/skills/memory-safety-c-cpp/SKILL.md +1 -0
- package/src/workflow/workflows/vuln-discovery/skills/vulnerability-triage/SKILL.md +4 -0
- package/src/workflow/workflows/vuln-discovery/workflow.yaml +13 -8
- package/dist/web-ui-static/assets/index-BFO5z1js.js +0 -76
- package/dist/web-ui-static/assets/index-CCUwrkIL.css +0 -1
|
@@ -14,13 +14,14 @@ import { quote } from 'shell-quote';
|
|
|
14
14
|
import { getBundleRuntimeRoot } from '../config/paths.js';
|
|
15
15
|
import { getBundleShortId } from '../session/types.js';
|
|
16
16
|
import { DEFAULT_CONTAINER_SCOPE } from '../workflow/types.js';
|
|
17
|
-
import { CONTAINER_WORKSPACE_DIR, } from './agent-adapter.js';
|
|
17
|
+
import { CONTAINER_SCRIPTS_DIR, CONTAINER_WORKSPACE_DIR, } from './agent-adapter.js';
|
|
18
18
|
import { parseUpstreamBaseUrl } from './provider-config.js';
|
|
19
19
|
import { getInternalNetworkName } from './platform.js';
|
|
20
20
|
import { cleanupContainers } from './container-lifecycle.js';
|
|
21
21
|
import { clampDockerResources } from './resource-limits.js';
|
|
22
22
|
import { errorMessage } from '../utils/error-message.js';
|
|
23
23
|
import { createCachedStager } from '../skills/staging.js';
|
|
24
|
+
import { withProvisionLock } from './provision-lock.js';
|
|
24
25
|
import * as logger from '../logger.js';
|
|
25
26
|
/**
|
|
26
27
|
* Create a bundle-owned directory and enforce 0o700 permissions even if
|
|
@@ -64,29 +65,19 @@ export function ensureSecureBundleDir(path) {
|
|
|
64
65
|
}
|
|
65
66
|
/** Hosts that use Anthropic OAuth credentials when available. */
|
|
66
67
|
const ANTHROPIC_HOSTS = new Set(['api.anthropic.com', 'platform.claude.com']);
|
|
68
|
+
/** Hosts that use Codex ChatGPT OAuth credentials when available. */
|
|
69
|
+
const CODEX_CHATGPT_HOSTS = new Set(['chatgpt.com', 'auth.openai.com']);
|
|
67
70
|
/** Prefix for container/sidecar names. Keep in sync with `docker ps` filters. */
|
|
68
71
|
const CONTAINER_NAME_PREFIX = 'ironcurtain-';
|
|
69
72
|
/** Host gateway alias used by Docker containers on macOS/Windows. */
|
|
70
73
|
const DOCKER_HOST_GATEWAY = 'host.docker.internal';
|
|
71
74
|
/** Bundle-relative subdir name for the skills staging dir. */
|
|
72
75
|
const BUNDLE_SKILLS_SUBDIR = 'skills';
|
|
73
|
-
/**
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* specific to the session mode: standalone sessions go through
|
|
79
|
-
* `createDockerInfrastructure()` (which wraps this with `sleep infinity`
|
|
80
|
-
* container creation); PTY sessions call this directly and then create their
|
|
81
|
-
* own TTY-enabled container.
|
|
82
|
-
*
|
|
83
|
-
* `workflowId` drives Docker labelling: when set, the main container (and
|
|
84
|
-
* any sidecar created during `createSessionContainers`) carries
|
|
85
|
-
* `ironcurtain.workflow=<workflowId>` + `ironcurtain.scope=<scope>`
|
|
86
|
-
* alongside the always-present `ironcurtain.bundle=<bundleId>`. When
|
|
87
|
-
* unset, only `ironcurtain.bundle` is emitted (standalone CLI / PTY).
|
|
88
|
-
*/
|
|
89
|
-
export async function prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills) {
|
|
76
|
+
/** Container path for runtime-provisioned workflow Python dependencies. */
|
|
77
|
+
const WORKFLOW_PYTHON_VENV_DIR = '/opt/workflow-venv';
|
|
78
|
+
/** Container path for runtime-provisioned workflow Node dependencies. */
|
|
79
|
+
const WORKFLOW_NODE_MODULES_DIR = '/opt/workflow-node_modules';
|
|
80
|
+
export async function prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir) {
|
|
90
81
|
// The audit log path is read from config so the bundle is
|
|
91
82
|
// self-describing: downstream consumers (AuditLogTailer, sandbox
|
|
92
83
|
// coordinator) can take it from either `config.auditLogPath` or
|
|
@@ -98,16 +89,18 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
98
89
|
const { createMitmProxy } = await import('./mitm-proxy.js');
|
|
99
90
|
const { loadOrCreateCA } = await import('./ca.js');
|
|
100
91
|
const { generateFakeKey } = await import('./fake-keys.js');
|
|
101
|
-
const {
|
|
102
|
-
const {
|
|
92
|
+
const { createContainerRuntime, resolveRuntimeKind } = await import('./container-runtime.js');
|
|
93
|
+
const { resolveNetworkTopology, createHostOnlyNetwork, makeSourceAddressGuard } = await import('./network-topology.js');
|
|
103
94
|
const { getIronCurtainHome } = await import('../config/paths.js');
|
|
104
95
|
const { prepareSession } = await import('./orientation.js');
|
|
105
|
-
const { detectAuthMethod, writeToKeychain } = await import('./oauth-credentials.js');
|
|
96
|
+
const { detectAuthMethod, writeToKeychain, getCodexAuthFilePath, loadCodexOAuthCredentials, refreshCodexOAuthToken, refreshResultToCreds, saveCodexOAuthCredentials, } = await import('./oauth-credentials.js');
|
|
106
97
|
const { OAuthTokenManager } = await import('./oauth-token-manager.js');
|
|
107
98
|
const { getBundleSocketsDir, getBundleHostOnlyDir, getBundleProxySocketPath, getBundleMitmProxySocketPath, getBundleMitmControlSocketPath, } = await import('../config/paths.js');
|
|
108
99
|
await registerBuiltinAdapters(config.userConfig);
|
|
109
100
|
const adapter = getAgent(mode.agent);
|
|
110
|
-
const
|
|
101
|
+
const runtimeKind = await resolveRuntimeKind(config.userConfig.containerRuntime);
|
|
102
|
+
const topology = resolveNetworkTopology(runtimeKind);
|
|
103
|
+
const useTcp = topology !== 'uds';
|
|
111
104
|
// Detect authentication method. Adapters with detectCredential() handle
|
|
112
105
|
// their own credential detection (e.g., Goose checks provider-specific keys).
|
|
113
106
|
// Adapters without it fall back to detectAuthMethod() (Anthropic OAuth + API key).
|
|
@@ -142,10 +135,26 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
142
135
|
ensureSecureBundleDir(socketsDir);
|
|
143
136
|
ensureSecureBundleDir(hostOnlyDir);
|
|
144
137
|
const socketPath = getBundleProxySocketPath(bundleId);
|
|
138
|
+
const docker = createContainerRuntime(runtimeKind);
|
|
139
|
+
// tcp-hostonly: create the per-bundle host-only network BEFORE the
|
|
140
|
+
// proxies are constructed. The gateway address feeds the container env,
|
|
141
|
+
// the orientation proxy address, and the connection-source guard both
|
|
142
|
+
// proxies use while listening on 0.0.0.0 (the vmnet gateway interface
|
|
143
|
+
// only materializes once the first container attaches, so binding the
|
|
144
|
+
// gateway address directly is not possible at this point).
|
|
145
|
+
let hostOnlyNetwork;
|
|
146
|
+
let allowRemoteAddress;
|
|
147
|
+
if (topology === 'tcp-hostonly') {
|
|
148
|
+
hostOnlyNetwork = await createHostOnlyNetwork(docker, getInternalNetworkName(getBundleShortId(bundleId)));
|
|
149
|
+
allowRemoteAddress = makeSourceAddressGuard(hostOnlyNetwork.subnet);
|
|
150
|
+
logger.info(`Host-only network ${hostOnlyNetwork.name} (${hostOnlyNetwork.subnet}, gateway ${hostOnlyNetwork.gateway})`);
|
|
151
|
+
}
|
|
145
152
|
const proxy = createCodeModeProxy({
|
|
146
153
|
socketPath,
|
|
147
154
|
config,
|
|
148
155
|
listenMode: useTcp ? 'tcp' : 'uds',
|
|
156
|
+
bindHost: topology === 'tcp-hostonly' ? '0.0.0.0' : undefined,
|
|
157
|
+
allowRemoteAddress,
|
|
149
158
|
});
|
|
150
159
|
// Load or generate the IronCurtain CA for TLS termination
|
|
151
160
|
const caDir = resolve(getIronCurtainHome(), 'ca');
|
|
@@ -158,8 +167,19 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
158
167
|
const tokenManagerKeychainDeps = authMethod.kind === 'oauth' && authMethod.source === 'keychain'
|
|
159
168
|
? { writeToKeychain, keychainServiceName: authMethod.keychainServiceName }
|
|
160
169
|
: undefined;
|
|
170
|
+
const tokenManagerCodexDeps = authMethod.kind === 'oauth' && adapter.id === 'codex'
|
|
171
|
+
? {
|
|
172
|
+
loadCredentials: loadCodexOAuthCredentials,
|
|
173
|
+
refreshToken: async (rt) => refreshResultToCreds(await refreshCodexOAuthToken(rt)),
|
|
174
|
+
saveCredentials: saveCodexOAuthCredentials,
|
|
175
|
+
credentialsFilePath: getCodexAuthFilePath(),
|
|
176
|
+
}
|
|
177
|
+
: undefined;
|
|
161
178
|
const tokenManager = authMethod.kind === 'oauth'
|
|
162
|
-
? new OAuthTokenManager(authMethod.credentials, { canRefresh:
|
|
179
|
+
? new OAuthTokenManager(authMethod.credentials, { canRefresh: canRefreshOAuth(authMethod.credentials.refreshToken) }, {
|
|
180
|
+
...tokenManagerKeychainDeps,
|
|
181
|
+
...tokenManagerCodexDeps,
|
|
182
|
+
})
|
|
163
183
|
: undefined;
|
|
164
184
|
const providers = adapter.getProviders(authKind);
|
|
165
185
|
const resolvedProviders = applyUpstreamOverrides(providers, parseUpstreamBaseUrl, {
|
|
@@ -178,8 +198,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
178
198
|
}
|
|
179
199
|
fakeKeys.set(providerConfig.host, fakeKey);
|
|
180
200
|
const realKey = resolveRealKey(providerConfig.host, config, oauthAccessToken);
|
|
181
|
-
|
|
182
|
-
|
|
201
|
+
const isManagedOAuthHost = ANTHROPIC_HOSTS.has(providerConfig.host) ||
|
|
202
|
+
(adapter.id === 'codex' && CODEX_CHATGPT_HOSTS.has(providerConfig.host));
|
|
203
|
+
const hostTokenManager = tokenManager && isManagedOAuthHost ? tokenManager : undefined;
|
|
183
204
|
providerMappings.push({ config: providerConfig, fakeKey, realKey, tokenManager: hostTokenManager });
|
|
184
205
|
}
|
|
185
206
|
// Build package installation proxy config if enabled
|
|
@@ -187,9 +208,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
187
208
|
let registries;
|
|
188
209
|
let packageValidation;
|
|
189
210
|
if (pkgConfig.enabled) {
|
|
190
|
-
const { npmRegistry, pypiRegistry, debianRegistry } = await import('./registry-proxy.js');
|
|
211
|
+
const { npmRegistry, pypiRegistry, debianRegistry, cargoRegistry } = await import('./registry-proxy.js');
|
|
191
212
|
const { createPackageValidator } = await import('./package-validator.js');
|
|
192
|
-
registries = [npmRegistry, pypiRegistry, debianRegistry];
|
|
213
|
+
registries = [npmRegistry, pypiRegistry, debianRegistry, cargoRegistry];
|
|
193
214
|
const validator = createPackageValidator({
|
|
194
215
|
allowedPackages: pkgConfig.allowedPackages,
|
|
195
216
|
deniedPackages: pkgConfig.deniedPackages,
|
|
@@ -208,6 +229,28 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
208
229
|
// A workflow bundle serves only workflow agents for its entire lifetime,
|
|
209
230
|
// so agentKind is fixed at construction time.
|
|
210
231
|
const agentKind = workflowId !== undefined ? 'workflow' : undefined;
|
|
232
|
+
// Single resolution point for trajectory-capture enablement. The raw
|
|
233
|
+
// CLI/RPC override wins; otherwise fall through to config; otherwise
|
|
234
|
+
// off. Consumers pass the raw override only — they never re-resolve
|
|
235
|
+
// against `userConfig.capture?.enabled`.
|
|
236
|
+
const captureEnabled = captureInput ? (captureInput.override ?? config.userConfig.capture?.enabled ?? false) : false;
|
|
237
|
+
// Construct the trajectory-capture writer when capture is enabled.
|
|
238
|
+
// When disabled, no writer is created, no taps are installed, and the
|
|
239
|
+
// forwarding path is byte-identical to today (per §10 "zero cost when
|
|
240
|
+
// disabled").
|
|
241
|
+
let captureWriter;
|
|
242
|
+
if (captureEnabled && captureInput) {
|
|
243
|
+
const { createTrajectoryCaptureWriter } = await import('./trajectory-capture.js');
|
|
244
|
+
captureWriter = createTrajectoryCaptureWriter({ capturesDir: captureInput.capturesDir });
|
|
245
|
+
}
|
|
246
|
+
const captureProxyOptions = captureWriter
|
|
247
|
+
? {
|
|
248
|
+
capture: captureWriter,
|
|
249
|
+
recordedAgentName: captureInput?.recordedAgentName,
|
|
250
|
+
workflowRunId: captureInput?.workflowRunId,
|
|
251
|
+
bundleId: String(bundleId),
|
|
252
|
+
}
|
|
253
|
+
: {};
|
|
211
254
|
const mitmProxy = useTcp
|
|
212
255
|
? createMitmProxy({
|
|
213
256
|
listenPort: 0,
|
|
@@ -216,8 +259,10 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
216
259
|
registries,
|
|
217
260
|
packageValidation,
|
|
218
261
|
controlPort: 0,
|
|
219
|
-
|
|
262
|
+
initialTokenSessionId: routingId,
|
|
220
263
|
agentKind,
|
|
264
|
+
allowRemoteAddress,
|
|
265
|
+
...captureProxyOptions,
|
|
221
266
|
})
|
|
222
267
|
: createMitmProxy({
|
|
223
268
|
socketPath: getBundleMitmProxySocketPath(bundleId),
|
|
@@ -226,16 +271,18 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
226
271
|
registries,
|
|
227
272
|
packageValidation,
|
|
228
273
|
controlSocketPath: getBundleMitmControlSocketPath(bundleId),
|
|
229
|
-
|
|
274
|
+
initialTokenSessionId: routingId,
|
|
230
275
|
agentKind,
|
|
276
|
+
...captureProxyOptions,
|
|
231
277
|
});
|
|
232
|
-
const docker = createDockerManager();
|
|
233
278
|
// Start MITM proxy FIRST so config.mitmControlAddr is set before proxy.start().
|
|
234
279
|
// proxy.start() initializes the UTCP sandbox, which checks config.mitmControlAddr
|
|
235
280
|
// to decide whether to register the proxy virtual MCP server for domain management.
|
|
236
281
|
const mitmAddr = await mitmProxy.start();
|
|
237
282
|
if (mitmAddr.port !== undefined) {
|
|
238
|
-
logger.info(
|
|
283
|
+
logger.info(hostOnlyNetwork
|
|
284
|
+
? `MITM proxy listening on ${hostOnlyNetwork.gateway}:${mitmAddr.port} (0.0.0.0, subnet-guarded)`
|
|
285
|
+
: `MITM proxy listening on 127.0.0.1:${mitmAddr.port}`);
|
|
239
286
|
}
|
|
240
287
|
else {
|
|
241
288
|
logger.info(`MITM proxy listening on ${mitmAddr.socketPath}`);
|
|
@@ -254,7 +301,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
254
301
|
// registers the proxy virtual server for network domain management.
|
|
255
302
|
await proxy.start();
|
|
256
303
|
if (useTcp && proxy.port !== undefined) {
|
|
257
|
-
logger.info(
|
|
304
|
+
logger.info(hostOnlyNetwork
|
|
305
|
+
? `Code Mode proxy listening on ${hostOnlyNetwork.gateway}:${proxy.port} (0.0.0.0, subnet-guarded)`
|
|
306
|
+
: `Code Mode proxy listening on 127.0.0.1:${proxy.port}`);
|
|
258
307
|
}
|
|
259
308
|
else {
|
|
260
309
|
logger.info(`Code Mode proxy listening on ${proxy.socketPath}`);
|
|
@@ -277,11 +326,18 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
277
326
|
});
|
|
278
327
|
}
|
|
279
328
|
logger.info(`Available servers: ${serverListings.map((s) => s.name).join(', ')}`);
|
|
280
|
-
|
|
329
|
+
// The address the agent uses to reach the Code Mode proxy: the vmnet
|
|
330
|
+
// gateway on host-only networks, the Docker host alias otherwise.
|
|
331
|
+
const proxyHost = hostOnlyNetwork ? hostOnlyNetwork.gateway : DOCKER_HOST_GATEWAY;
|
|
332
|
+
const proxyAddress = useTcp && proxy.port !== undefined ? `${proxyHost}:${proxy.port}` : undefined;
|
|
281
333
|
const { systemPrompt } = prepareSession(adapter, serverListings, bundleDir, config, workspaceDir, proxyAddress);
|
|
282
|
-
// Ensure
|
|
283
|
-
|
|
284
|
-
|
|
334
|
+
// Ensure the stock agent image is built and up-to-date. Workflow
|
|
335
|
+
// dependencies are provisioned at runtime into mounted caches below;
|
|
336
|
+
// they are no longer baked into per-workflow Docker images.
|
|
337
|
+
const agentImage = await adapter.getImage();
|
|
338
|
+
const agentBuildHash = await ensureImage(agentImage, docker, ca);
|
|
339
|
+
const image = agentImage;
|
|
340
|
+
const workflowDependencyMounts = prepareWorkflowDependencyMounts(agentBuildHash, scriptsDir, getIronCurtainHome());
|
|
285
341
|
const orientationDir = resolve(bundleDir, 'orientation');
|
|
286
342
|
// Set up conversation state directory if the adapter supports resume
|
|
287
343
|
const conversationStateConfig = adapter.getConversationStateConfig?.();
|
|
@@ -312,6 +368,11 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
312
368
|
logger.info(`Re-staged ${skills.length} skill(s) to ${skillsMount.hostDir}`);
|
|
313
369
|
}
|
|
314
370
|
};
|
|
371
|
+
let scriptsMount;
|
|
372
|
+
if (scriptsDir !== undefined && existsSync(scriptsDir)) {
|
|
373
|
+
scriptsMount = { hostDir: scriptsDir, target: CONTAINER_SCRIPTS_DIR };
|
|
374
|
+
logger.info(`Staged workflow scripts available at ${scriptsDir}`);
|
|
375
|
+
}
|
|
315
376
|
return {
|
|
316
377
|
bundleId,
|
|
317
378
|
workflowId,
|
|
@@ -329,23 +390,62 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
329
390
|
orientationDir,
|
|
330
391
|
systemPrompt,
|
|
331
392
|
image,
|
|
393
|
+
runtimeKind,
|
|
394
|
+
topology,
|
|
332
395
|
useTcp,
|
|
396
|
+
hostOnlyNetwork,
|
|
333
397
|
socketsDir,
|
|
334
398
|
mitmAddr,
|
|
335
399
|
authKind,
|
|
336
400
|
conversationStateDir,
|
|
337
401
|
conversationStateConfig,
|
|
338
402
|
skillsMount,
|
|
403
|
+
scriptsMount,
|
|
404
|
+
...workflowDependencyMounts,
|
|
339
405
|
restageSkills,
|
|
340
406
|
setTokenSessionId: (id) => {
|
|
341
407
|
mitmProxy.setTokenSessionId(id);
|
|
342
408
|
},
|
|
409
|
+
// Trajectory-capture lifecycle. When captureWriter is undefined
|
|
410
|
+
// (capture disabled, the common case), every method is a cheap
|
|
411
|
+
// no-op — zero cost on the forwarding path. When set, the bundle
|
|
412
|
+
// owns the three-step atomic begin (setCaptureSessionId →
|
|
413
|
+
// setCapturePersona → writer.beginSession) and the two-phase end
|
|
414
|
+
// (writer.endSession → null out proxy attribution).
|
|
415
|
+
beginCaptureSession: (opts) => {
|
|
416
|
+
if (!captureWriter)
|
|
417
|
+
return;
|
|
418
|
+
mitmProxy.setCaptureSessionId(opts.sessionId);
|
|
419
|
+
mitmProxy.setCapturePersona(opts.persona);
|
|
420
|
+
captureWriter.beginSession(opts);
|
|
421
|
+
},
|
|
422
|
+
endCaptureSession: async (sessionId) => {
|
|
423
|
+
if (!captureWriter)
|
|
424
|
+
return;
|
|
425
|
+
try {
|
|
426
|
+
await captureWriter.endSession(sessionId);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
// Clear proxy attribution AFTER the drain settles so any
|
|
430
|
+
// late-arriving response chunks already in flight are still
|
|
431
|
+
// attributed to the correct session.
|
|
432
|
+
mitmProxy.setCaptureSessionId(undefined);
|
|
433
|
+
mitmProxy.setCapturePersona(undefined);
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
captureWriter,
|
|
343
437
|
};
|
|
344
438
|
}
|
|
345
439
|
catch (error) {
|
|
346
440
|
// Best-effort cleanup of proxies started above
|
|
347
441
|
await mitmProxy.stop().catch(() => { });
|
|
348
442
|
await proxy.stop().catch(() => { });
|
|
443
|
+
// Host-only network was created before the proxies; remove it too.
|
|
444
|
+
// (A leak through the narrow window before this catch is self-healing:
|
|
445
|
+
// createHostOnlyNetwork removes the stale same-named network first.)
|
|
446
|
+
if (hostOnlyNetwork) {
|
|
447
|
+
await docker.removeNetwork(hostOnlyNetwork.name).catch(() => { });
|
|
448
|
+
}
|
|
349
449
|
throw error;
|
|
350
450
|
}
|
|
351
451
|
}
|
|
@@ -358,16 +458,27 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
358
458
|
* failure after the proxies are started, all started resources are torn
|
|
359
459
|
* down before the error propagates.
|
|
360
460
|
*/
|
|
361
|
-
export async function createDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills) {
|
|
362
|
-
const core = await prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills);
|
|
461
|
+
export async function createDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir, options) {
|
|
462
|
+
const core = await prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir);
|
|
463
|
+
let containerResources;
|
|
363
464
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
465
|
+
containerResources = await createSessionContainers(core, config, options);
|
|
466
|
+
const infra = { ...core, ...containerResources };
|
|
467
|
+
await provisionWorkflowDependencies(infra, config.userConfig.packageInstall.enabled);
|
|
468
|
+
return infra;
|
|
366
469
|
}
|
|
367
470
|
catch (error) {
|
|
368
471
|
// Any partial container/sidecar/network cleanup happened inside
|
|
369
|
-
// createSessionContainers().
|
|
370
|
-
//
|
|
472
|
+
// createSessionContainers(). If provisioning failed after a complete
|
|
473
|
+
// container bundle was created, clean that bundle here before tearing
|
|
474
|
+
// down the proxies.
|
|
475
|
+
if (containerResources) {
|
|
476
|
+
await cleanupContainers(core.docker, {
|
|
477
|
+
containerId: containerResources.containerId,
|
|
478
|
+
sidecarContainerId: containerResources.sidecarContainerId ?? null,
|
|
479
|
+
networkName: containerResources.internalNetwork ?? null,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
371
482
|
await core.mitmProxy.stop().catch(() => { });
|
|
372
483
|
await core.proxy.stop().catch(() => { });
|
|
373
484
|
throw error;
|
|
@@ -409,6 +520,17 @@ export async function destroyDockerInfrastructure(infra) {
|
|
|
409
520
|
.stop()
|
|
410
521
|
.catch((err) => logger.warn(`destroyDockerInfrastructure: proxy.stop() failed: ${errorMessage(err)}`)),
|
|
411
522
|
]);
|
|
523
|
+
// Trajectory-capture safety net (§9): close the writer AFTER the proxies
|
|
524
|
+
// have stopped, so no more records arrive mid-close. The writer emits
|
|
525
|
+
// synthetic `session-end` entries (with `closedReason:
|
|
526
|
+
// 'infrastructure-teardown'`) for any session whose explicit
|
|
527
|
+
// endCaptureSession was not called — covering Ctrl-C / abort / crash
|
|
528
|
+
// paths where the orchestrator's `finally` did not run.
|
|
529
|
+
if (infra.captureWriter) {
|
|
530
|
+
await infra.captureWriter
|
|
531
|
+
.close()
|
|
532
|
+
.catch((err) => logger.warn(`destroyDockerInfrastructure: captureWriter.close() failed: ${errorMessage(err)}`));
|
|
533
|
+
}
|
|
412
534
|
// CA and fake keys are intentionally absent: neither owns any
|
|
413
535
|
// process-level resources. CA material is persisted in ~/.ironcurtain/ca/
|
|
414
536
|
// and reused across sessions; fake keys are just strings in a Map that
|
|
@@ -488,9 +610,9 @@ export function buildAgentUidRemap(useTcp) {
|
|
|
488
610
|
*
|
|
489
611
|
* Exported for testability: tests exercise the mount/env configuration and
|
|
490
612
|
* the rollback-on-failure path by passing a mock `PreContainerInfrastructure`
|
|
491
|
-
* with a scripted `
|
|
613
|
+
* with a scripted `ContainerRuntime`.
|
|
492
614
|
*/
|
|
493
|
-
export async function createSessionContainers(core, config) {
|
|
615
|
+
export async function createSessionContainers(core, config, options) {
|
|
494
616
|
const shortId = getBundleShortId(core.bundleId);
|
|
495
617
|
const mainContainerName = `${CONTAINER_NAME_PREFIX}${shortId}`;
|
|
496
618
|
// Labels applied to every IronCurtain-owned container (main + sidecar).
|
|
@@ -507,6 +629,9 @@ export async function createSessionContainers(core, config) {
|
|
|
507
629
|
let sidecarContainerId;
|
|
508
630
|
let internalNetwork;
|
|
509
631
|
try {
|
|
632
|
+
const mainImage = options?.baseImageOverride && (await core.docker.imageExists(options.baseImageOverride))
|
|
633
|
+
? options.baseImageOverride
|
|
634
|
+
: core.image;
|
|
510
635
|
// Base mounts shared by TCP and UDS modes: the sandbox as the
|
|
511
636
|
// workspace and the orientation dir. Mode-specific mounts (apt proxy
|
|
512
637
|
// config, sockets dir, conversation state) are appended below.
|
|
@@ -519,7 +644,34 @@ export async function createSessionContainers(core, config) {
|
|
|
519
644
|
};
|
|
520
645
|
let network;
|
|
521
646
|
let extraHosts;
|
|
522
|
-
|
|
647
|
+
// tcp-hostonly only: apt proxy config to write into the container via
|
|
648
|
+
// exec after start. Apple container's virtiofs shares directories
|
|
649
|
+
// only — the single-file bind mount the Docker topologies use for
|
|
650
|
+
// /etc/apt/apt.conf.d/90-ironcurtain-proxy is rejected with
|
|
651
|
+
// "path ... is not a directory".
|
|
652
|
+
let hostOnlyAptProxyUrl;
|
|
653
|
+
if (core.topology === 'tcp-hostonly') {
|
|
654
|
+
// Apple container host-only mode: the agent VM reaches the host
|
|
655
|
+
// proxies directly at the vmnet gateway address. No sidecar, no
|
|
656
|
+
// extra host mappings — egress is blocked at the network layer
|
|
657
|
+
// (`--internal`) and verified by the connectivity check below.
|
|
658
|
+
if (core.hostOnlyNetwork === undefined || core.mitmAddr.port === undefined || core.proxy.port === undefined) {
|
|
659
|
+
throw new Error('tcp-hostonly bundle is missing its host-only network or proxy ports');
|
|
660
|
+
}
|
|
661
|
+
const proxyUrl = `http://${core.hostOnlyNetwork.gateway}:${core.mitmAddr.port}`;
|
|
662
|
+
env = {
|
|
663
|
+
...env,
|
|
664
|
+
HTTPS_PROXY: proxyUrl,
|
|
665
|
+
HTTP_PROXY: proxyUrl,
|
|
666
|
+
};
|
|
667
|
+
hostOnlyAptProxyUrl = proxyUrl;
|
|
668
|
+
network = core.hostOnlyNetwork.name;
|
|
669
|
+
// Report the host-only network as `internalNetwork` so the standard
|
|
670
|
+
// teardown paths (destroyDockerInfrastructure, rollback below)
|
|
671
|
+
// remove it with the containers.
|
|
672
|
+
internalNetwork = core.hostOnlyNetwork.name;
|
|
673
|
+
}
|
|
674
|
+
else if (core.topology === 'tcp-sidecar' && core.mitmAddr.port !== undefined && core.proxy.port !== undefined) {
|
|
523
675
|
// macOS TCP mode: internal bridge network blocks egress.
|
|
524
676
|
// A socat sidecar bridges the internal network to the host
|
|
525
677
|
// because Docker Desktop VMs don't forward gateway traffic.
|
|
@@ -614,6 +766,23 @@ export async function createSessionContainers(core, config) {
|
|
|
614
766
|
if (core.skillsMount) {
|
|
615
767
|
mounts.push({ source: core.skillsMount.hostDir, target: core.skillsMount.target, readonly: true });
|
|
616
768
|
}
|
|
769
|
+
if (core.scriptsMount) {
|
|
770
|
+
mounts.push({ source: core.scriptsMount.hostDir, target: core.scriptsMount.target, readonly: true });
|
|
771
|
+
}
|
|
772
|
+
if (core.workflowPythonVenvMount) {
|
|
773
|
+
mounts.push({
|
|
774
|
+
source: core.workflowPythonVenvMount.hostDir,
|
|
775
|
+
target: core.workflowPythonVenvMount.target,
|
|
776
|
+
readonly: false,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
if (core.workflowNodeModulesMount) {
|
|
780
|
+
mounts.push({
|
|
781
|
+
source: core.workflowNodeModulesMount.hostDir,
|
|
782
|
+
target: core.workflowNodeModulesMount.target,
|
|
783
|
+
readonly: false,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
617
786
|
// Linux-only UID-remap wiring (issue #232). On Linux, run the
|
|
618
787
|
// container as root and pass the host UID/GID via env so the
|
|
619
788
|
// entrypoint can renumber codespace before dropping privileges.
|
|
@@ -627,11 +796,22 @@ export async function createSessionContainers(core, config) {
|
|
|
627
796
|
// "no flag emitted" (see clampDockerResources docs).
|
|
628
797
|
const { effective: containerResources } = clampDockerResources(config.userConfig.dockerResources);
|
|
629
798
|
mainContainerId = await core.docker.create({
|
|
630
|
-
image:
|
|
799
|
+
image: mainImage,
|
|
631
800
|
name: mainContainerName,
|
|
632
801
|
network: network ?? 'none',
|
|
633
802
|
mounts,
|
|
634
|
-
env: {
|
|
803
|
+
env: {
|
|
804
|
+
...env,
|
|
805
|
+
// Do NOT override PATH here. Docker `-e PATH=...` REPLACES the image's
|
|
806
|
+
// PATH (it does not append), which would discard the base image's real
|
|
807
|
+
// PATH — including the NVM directory where `node`/`npm` live on the x86
|
|
808
|
+
// devcontainer base. Bare-`node` workflow helpers would then fail to
|
|
809
|
+
// resolve. The workflow venv bin is instead prepended to the live
|
|
810
|
+
// `$PATH` at exec time (see buildWorkflowExecCommand), which is
|
|
811
|
+
// base-image-agnostic and preserves the image's own PATH.
|
|
812
|
+
...(core.workflowNodeModulesMount ? { NODE_PATH: core.workflowNodeModulesMount.target } : {}),
|
|
813
|
+
...uidRemap.env,
|
|
814
|
+
},
|
|
635
815
|
user: uidRemap.user,
|
|
636
816
|
command: ['sleep', 'infinity'],
|
|
637
817
|
...bundleLabels,
|
|
@@ -648,9 +828,19 @@ export async function createSessionContainers(core, config) {
|
|
|
648
828
|
});
|
|
649
829
|
await core.docker.start(mainContainerId);
|
|
650
830
|
logger.info(`Container started: ${mainContainerId.substring(0, 12)}`);
|
|
831
|
+
// tcp-hostonly: write the apt proxy config inside the container (the
|
|
832
|
+
// Docker topologies bind-mount it; see hostOnlyAptProxyUrl above).
|
|
833
|
+
if (hostOnlyAptProxyUrl !== undefined) {
|
|
834
|
+
await writeHostOnlyAptProxyConfig(core.docker, mainContainerId, hostOnlyAptProxyUrl);
|
|
835
|
+
}
|
|
651
836
|
// Connectivity check: verify the container can reach host proxies
|
|
652
|
-
// through the internal network. Abort if unreachable.
|
|
653
|
-
|
|
837
|
+
// through the internal network. Abort if unreachable. Host-only
|
|
838
|
+
// bundles additionally assert the inverse — internet egress must be
|
|
839
|
+
// blocked — and never fall back to a weaker configuration.
|
|
840
|
+
if (core.topology === 'tcp-hostonly' && core.hostOnlyNetwork !== undefined && core.proxy.port !== undefined) {
|
|
841
|
+
await checkHostOnlyConnectivity(core.docker, mainContainerId, core.hostOnlyNetwork.gateway, core.proxy.port);
|
|
842
|
+
}
|
|
843
|
+
else if (core.useTcp && internalNetwork !== undefined && core.proxy.port !== undefined) {
|
|
654
844
|
await checkInternalNetworkConnectivity(core.docker, mainContainerId, core.proxy.port);
|
|
655
845
|
}
|
|
656
846
|
return {
|
|
@@ -687,16 +877,76 @@ async function checkInternalNetworkConnectivity(docker, containerId, mcpPort) {
|
|
|
687
877
|
`Check that the sidecar container is running and connected to the internal network.`);
|
|
688
878
|
}
|
|
689
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Writes /etc/apt/apt.conf.d/90-ironcurtain-proxy inside a running
|
|
882
|
+
* container via exec (as root). Used by the tcp-hostonly topology in
|
|
883
|
+
* both batch and PTY modes — Apple container's virtiofs shares
|
|
884
|
+
* directories only, so the single-file bind mount the Docker topologies
|
|
885
|
+
* use is rejected. The URL is built from our own gateway address and
|
|
886
|
+
* OS-assigned port — runtime-generated values, not untrusted input — so
|
|
887
|
+
* embedding it in the sh script is safe.
|
|
888
|
+
*/
|
|
889
|
+
export async function writeHostOnlyAptProxyConfig(docker, containerId, proxyUrl) {
|
|
890
|
+
const aptWrite = await docker.exec(containerId, [
|
|
891
|
+
'sh',
|
|
892
|
+
'-c',
|
|
893
|
+
`printf 'Acquire::http::Proxy "%s";\\nAcquire::https::Proxy "%s";\\n' '${proxyUrl}' '${proxyUrl}' > /etc/apt/apt.conf.d/90-ironcurtain-proxy`,
|
|
894
|
+
], 10_000, 'root');
|
|
895
|
+
if (aptWrite.exitCode !== 0) {
|
|
896
|
+
throw new Error(`Failed to write apt proxy config in container (exit=${aptWrite.exitCode}): ${aptWrite.stderr}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* External address used to probe that internet egress is blocked. Any
|
|
901
|
+
* globally-routable address works; the check asserts the connection
|
|
902
|
+
* FAILS, so the probe never carries data off the machine on a healthy
|
|
903
|
+
* setup.
|
|
904
|
+
*/
|
|
905
|
+
const EGRESS_PROBE_ADDRESS = '1.1.1.1:443';
|
|
906
|
+
/**
|
|
907
|
+
* Fail-closed startup gate for the tcp-hostonly topology
|
|
908
|
+
* (docs/designs/apple-container-runtime.md, design decision 4). Asserts
|
|
909
|
+
* from inside the container that (a) the host-side proxies are reachable
|
|
910
|
+
* at the vmnet gateway and (b) internet egress is blocked by the
|
|
911
|
+
* host-only network. Either failure aborts session initialization —
|
|
912
|
+
* never a silent fallback to a weaker network configuration. Shared by
|
|
913
|
+
* batch (`createSessionContainers`) and PTY (`runPtySession`) modes.
|
|
914
|
+
*/
|
|
915
|
+
export async function checkHostOnlyConnectivity(docker, containerId, gateway, mcpPort) {
|
|
916
|
+
const reach = await docker.exec(containerId, ['socat', '-u', '/dev/null', `TCP:${gateway}:${mcpPort},connect-timeout=5`], 6_000);
|
|
917
|
+
if (reach.exitCode !== 0) {
|
|
918
|
+
throw new Error(`Host-only network connectivity check failed (exit=${reach.exitCode}). ` +
|
|
919
|
+
`The container cannot reach host-side proxies at gateway ${gateway}:${mcpPort}. ` +
|
|
920
|
+
`Check that the host-only network exists and the proxies are listening.`);
|
|
921
|
+
}
|
|
922
|
+
const egress = await docker.exec(containerId, ['socat', '-u', '/dev/null', `TCP:${EGRESS_PROBE_ADDRESS},connect-timeout=3`], 5_000);
|
|
923
|
+
if (egress.exitCode === 0) {
|
|
924
|
+
throw new Error(`Host-only network egress check failed: the container reached ${EGRESS_PROBE_ADDRESS}. ` +
|
|
925
|
+
`The network is not blocking internet egress as required; refusing to start the session.`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Whether an OAuth credential set can be refreshed: true only when a
|
|
930
|
+
* non-empty refresh token is present. Externally-managed Codex tokens
|
|
931
|
+
* (`auth_mode: 'chatgptAuthTokens'`) carry an empty refresh token and must
|
|
932
|
+
* NOT be refreshed by IronCurtain. Pure helper, exported for testability.
|
|
933
|
+
*/
|
|
934
|
+
export function canRefreshOAuth(refreshToken) {
|
|
935
|
+
return refreshToken.length > 0;
|
|
936
|
+
}
|
|
690
937
|
/**
|
|
691
938
|
* Resolves the real credential for a provider host.
|
|
692
939
|
*
|
|
693
940
|
* For Anthropic hosts in OAuth mode, uses the OAuth access token.
|
|
694
941
|
* For all other cases, falls back to the API key from config.
|
|
695
942
|
*/
|
|
696
|
-
function resolveRealKey(host, config, oauthAccessToken) {
|
|
943
|
+
export function resolveRealKey(host, config, oauthAccessToken) {
|
|
697
944
|
if (oauthAccessToken && ANTHROPIC_HOSTS.has(host)) {
|
|
698
945
|
return oauthAccessToken;
|
|
699
946
|
}
|
|
947
|
+
if (oauthAccessToken && CODEX_CHATGPT_HOSTS.has(host)) {
|
|
948
|
+
return oauthAccessToken;
|
|
949
|
+
}
|
|
700
950
|
let key;
|
|
701
951
|
switch (host) {
|
|
702
952
|
case 'api.anthropic.com':
|
|
@@ -706,6 +956,10 @@ function resolveRealKey(host, config, oauthAccessToken) {
|
|
|
706
956
|
case 'api.openai.com':
|
|
707
957
|
key = config.userConfig.openaiApiKey;
|
|
708
958
|
break;
|
|
959
|
+
case 'chatgpt.com':
|
|
960
|
+
case 'auth.openai.com':
|
|
961
|
+
key = '';
|
|
962
|
+
break;
|
|
709
963
|
case 'generativelanguage.googleapis.com':
|
|
710
964
|
key = config.userConfig.googleApiKey;
|
|
711
965
|
break;
|
|
@@ -723,9 +977,13 @@ function resolveRealKey(host, config, oauthAccessToken) {
|
|
|
723
977
|
* support session resume. Idempotent: skips seeding if the directory
|
|
724
978
|
* already exists (resume case).
|
|
725
979
|
*
|
|
726
|
-
* As a defense-in-depth measure, always deletes
|
|
727
|
-
*
|
|
728
|
-
*
|
|
980
|
+
* As a defense-in-depth measure, always deletes stale credential files
|
|
981
|
+
* (`.credentials.json` for Claude Code, `auth.json` for Codex) from the
|
|
982
|
+
* state directory — the MITM proxy handles auth independently, and each
|
|
983
|
+
* agent's entrypoint recreates its credential file from env on every
|
|
984
|
+
* start, so any credential file lingering across resumes is stale. The
|
|
985
|
+
* unlinks are no-ops for adapters whose state dir has neither file (e.g.
|
|
986
|
+
* Goose), since a missing file is swallowed.
|
|
729
987
|
*/
|
|
730
988
|
export function prepareConversationStateDir(sessionDir, config) {
|
|
731
989
|
const stateDir = resolve(sessionDir, config.hostDirName);
|
|
@@ -752,13 +1010,16 @@ export function prepareConversationStateDir(sessionDir, config) {
|
|
|
752
1010
|
}
|
|
753
1011
|
}
|
|
754
1012
|
}
|
|
755
|
-
// Defense-in-depth: remove stale
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1013
|
+
// Defense-in-depth: remove stale credential files on every start. The
|
|
1014
|
+
// entrypoint recreates them from env each start, so scrubbing host-side
|
|
1015
|
+
// is safe. Both unlinks are no-ops when the file is absent.
|
|
1016
|
+
for (const fileName of ['.credentials.json', 'auth.json']) {
|
|
1017
|
+
try {
|
|
1018
|
+
unlinkSync(resolve(stateDir, fileName));
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
// File doesn't exist — expected on first run / other adapters
|
|
1022
|
+
}
|
|
762
1023
|
}
|
|
763
1024
|
return stateDir;
|
|
764
1025
|
}
|
|
@@ -777,12 +1038,12 @@ export function prepareConversationStateDir(sessionDir, config) {
|
|
|
777
1038
|
export async function ensureDockerImage(agentId, userConfig) {
|
|
778
1039
|
const { registerBuiltinAdapters, getAgent } = await import('./agent-registry.js');
|
|
779
1040
|
const { loadOrCreateCA } = await import('./ca.js');
|
|
780
|
-
const {
|
|
1041
|
+
const { createContainerRuntime, resolveRuntimeKind } = await import('./container-runtime.js');
|
|
781
1042
|
const { getIronCurtainHome } = await import('../config/paths.js');
|
|
782
1043
|
await registerBuiltinAdapters(userConfig);
|
|
783
1044
|
const adapter = getAgent(agentId);
|
|
784
1045
|
const image = await adapter.getImage();
|
|
785
|
-
const docker =
|
|
1046
|
+
const docker = createContainerRuntime(await resolveRuntimeKind(userConfig.containerRuntime));
|
|
786
1047
|
const ca = loadOrCreateCA(resolve(getIronCurtainHome(), 'ca'));
|
|
787
1048
|
await ensureImage(image, docker, ca);
|
|
788
1049
|
}
|
|
@@ -792,7 +1053,33 @@ export async function ensureDockerImage(agentId, userConfig) {
|
|
|
792
1053
|
* agent-specific image. Content-hash labels on each image drive staleness
|
|
793
1054
|
* detection so repeated calls skip rebuilds when nothing has changed.
|
|
794
1055
|
*/
|
|
795
|
-
|
|
1056
|
+
/**
|
|
1057
|
+
* Builds `image` from a fresh temp directory populated with the contents of
|
|
1058
|
+
* `dockerDir` (plus any `extraFiles`, keyed dest→src). Building from a clean
|
|
1059
|
+
* dir outside any git repo is REQUIRED for Apple `container build`, which
|
|
1060
|
+
* resolves an EMPTY context when handed a git-tracked source directory (the
|
|
1061
|
+
* repo's docker/ in a checkout/worktree) — making `COPY` fail with "not
|
|
1062
|
+
* found"; harmless on Docker. The Dockerfiles only COPY files that live in
|
|
1063
|
+
* `dockerDir` / `extraFiles`.
|
|
1064
|
+
*/
|
|
1065
|
+
async function buildImageFromCleanContext(docker, image, dockerDir, dockerfile, labels, extraFiles = {}) {
|
|
1066
|
+
const tmpContext = mkdtempSync(resolve(tmpdir(), 'ironcurtain-build-'));
|
|
1067
|
+
try {
|
|
1068
|
+
for (const file of readdirSync(dockerDir)) {
|
|
1069
|
+
copyFileSync(resolve(dockerDir, file), resolve(tmpContext, file));
|
|
1070
|
+
}
|
|
1071
|
+
for (const [dest, src] of Object.entries(extraFiles)) {
|
|
1072
|
+
copyFileSync(src, resolve(tmpContext, dest));
|
|
1073
|
+
}
|
|
1074
|
+
await docker.buildImage(image, resolve(tmpContext, dockerfile), tmpContext, labels);
|
|
1075
|
+
}
|
|
1076
|
+
finally {
|
|
1077
|
+
rmSync(tmpContext, { recursive: true, force: true });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
// `docker` is typed `ContainerRuntime` (the apple-container generalization of
|
|
1081
|
+
// the former `DockerManager`); exported because callers/tests import it.
|
|
1082
|
+
export async function ensureImage(image, docker, ca) {
|
|
796
1083
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
797
1084
|
const dockerDir = resolve(packageRoot, 'docker');
|
|
798
1085
|
// On arm64 hosts (Apple Silicon), use the lightweight arm64-native Dockerfile
|
|
@@ -814,29 +1101,186 @@ async function ensureImage(image, docker, ca) {
|
|
|
814
1101
|
const needsAgentBuild = baseRebuilt || (await isImageStale(image, docker, agentBuildHash));
|
|
815
1102
|
if (needsAgentBuild) {
|
|
816
1103
|
logger.info(`Building Docker image ${image}...`);
|
|
817
|
-
await docker
|
|
1104
|
+
await buildImageFromCleanContext(docker, image, dockerDir, dockerfile, {
|
|
818
1105
|
'ironcurtain.build-hash': agentBuildHash,
|
|
819
1106
|
});
|
|
820
1107
|
logger.info(`Docker image ${image} built successfully`);
|
|
821
1108
|
}
|
|
1109
|
+
return agentBuildHash;
|
|
1110
|
+
}
|
|
1111
|
+
export function computeWorkflowDependencyHash(agentBuildHash, scriptsDir) {
|
|
1112
|
+
const hash = createHash('sha256');
|
|
1113
|
+
hash.update(`agent:${agentBuildHash}\n`);
|
|
1114
|
+
for (const manifest of ['requirements.txt', 'package.json', 'package-lock.json']) {
|
|
1115
|
+
const manifestPath = resolve(scriptsDir, manifest);
|
|
1116
|
+
if (!existsSync(manifestPath))
|
|
1117
|
+
continue;
|
|
1118
|
+
hash.update(`file:${manifest}\n`);
|
|
1119
|
+
hash.update(readFileSync(manifestPath));
|
|
1120
|
+
hash.update('\n');
|
|
1121
|
+
}
|
|
1122
|
+
return hash.digest('hex');
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Wraps an in-container command so the workflow dependency bins are prepended
|
|
1126
|
+
* to the container's LIVE `$PATH` at exec time, rather than replacing the
|
|
1127
|
+
* image's PATH at container-creation time.
|
|
1128
|
+
*
|
|
1129
|
+
* Why a runtime shell instead of an `-e PATH=...` env: Docker `-e` REPLACES the
|
|
1130
|
+
* image PATH (it does not append), which would discard the base image's own
|
|
1131
|
+
* PATH — including the NVM directory where `node`/`npm` live on the x86
|
|
1132
|
+
* devcontainer base. Expanding `$PATH` inside the container at exec time is
|
|
1133
|
+
* base-image-agnostic: it preserves whatever PATH the image ships and merely
|
|
1134
|
+
* prepends the workflow venv bin (for bare `python`) and the installed Node
|
|
1135
|
+
* package bins (`node_modules/.bin`).
|
|
1136
|
+
*
|
|
1137
|
+
* Returns the original command unchanged when neither dependency mount is
|
|
1138
|
+
* present, so non-dependency workflows keep the plain exec path.
|
|
1139
|
+
*
|
|
1140
|
+
* Shell-safety: the only interpolated values are the hardcoded container
|
|
1141
|
+
* constants `WORKFLOW_PYTHON_VENV_DIR` / `WORKFLOW_NODE_MODULES_DIR`. The
|
|
1142
|
+
* caller's command and its arguments are passed verbatim as positional
|
|
1143
|
+
* parameters consumed by `exec "$@"` — never string-interpolated — so no
|
|
1144
|
+
* word-splitting or injection is possible.
|
|
1145
|
+
*/
|
|
1146
|
+
export function buildWorkflowExecCommand(bundle, command) {
|
|
1147
|
+
const prefixDirs = [];
|
|
1148
|
+
if (bundle.workflowPythonVenvMount)
|
|
1149
|
+
prefixDirs.push(`${WORKFLOW_PYTHON_VENV_DIR}/bin`);
|
|
1150
|
+
if (bundle.workflowNodeModulesMount)
|
|
1151
|
+
prefixDirs.push(`${WORKFLOW_NODE_MODULES_DIR}/.bin`);
|
|
1152
|
+
if (prefixDirs.length === 0 || command.length === 0)
|
|
1153
|
+
return command;
|
|
1154
|
+
const pathPrefix = prefixDirs.join(':');
|
|
1155
|
+
// `exec "$@"` runs the original argv verbatim; the leading `sh` is $0.
|
|
1156
|
+
return ['/bin/sh', '-lc', `export PATH=${pathPrefix}:"$PATH"; exec "$@"`, 'sh', ...command];
|
|
1157
|
+
}
|
|
1158
|
+
function prepareWorkflowDependencyMounts(agentBuildHash, scriptsDir, ironcurtainHome) {
|
|
1159
|
+
if (scriptsDir === undefined || !existsSync(scriptsDir))
|
|
1160
|
+
return {};
|
|
1161
|
+
const requirementsPath = resolve(scriptsDir, 'requirements.txt');
|
|
1162
|
+
const packageJsonPath = resolve(scriptsDir, 'package.json');
|
|
1163
|
+
const packageLockPath = resolve(scriptsDir, 'package-lock.json');
|
|
1164
|
+
const hasPythonManifest = existsSync(requirementsPath);
|
|
1165
|
+
const hasNodeManifest = existsSync(packageJsonPath);
|
|
1166
|
+
if (!hasPythonManifest && !hasNodeManifest)
|
|
1167
|
+
return {};
|
|
1168
|
+
const dependencyHash = computeWorkflowDependencyHash(agentBuildHash, scriptsDir);
|
|
1169
|
+
const cacheRoot = resolve(ironcurtainHome, 'workflow-deps', dependencyHash.slice(0, 24));
|
|
1170
|
+
mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
|
|
1171
|
+
let workflowPythonVenvMount;
|
|
1172
|
+
let workflowNodeModulesMount;
|
|
1173
|
+
if (hasPythonManifest) {
|
|
1174
|
+
const hostDir = resolve(cacheRoot, 'python-venv');
|
|
1175
|
+
mkdirSync(hostDir, { recursive: true, mode: 0o700 });
|
|
1176
|
+
workflowPythonVenvMount = {
|
|
1177
|
+
hostDir,
|
|
1178
|
+
target: WORKFLOW_PYTHON_VENV_DIR,
|
|
1179
|
+
cacheKey: dependencyHash,
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
if (hasNodeManifest) {
|
|
1183
|
+
const hostDir = resolve(cacheRoot, 'node_modules');
|
|
1184
|
+
mkdirSync(hostDir, { recursive: true, mode: 0o700 });
|
|
1185
|
+
workflowNodeModulesMount = {
|
|
1186
|
+
hostDir,
|
|
1187
|
+
target: WORKFLOW_NODE_MODULES_DIR,
|
|
1188
|
+
cacheKey: dependencyHash,
|
|
1189
|
+
hasPackageLock: existsSync(packageLockPath),
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
...(workflowPythonVenvMount ? { workflowPythonVenvMount } : {}),
|
|
1194
|
+
...(workflowNodeModulesMount ? { workflowNodeModulesMount } : {}),
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
async function provisionWorkflowDependencies(infra, packageInstallEnabled) {
|
|
1198
|
+
if (!infra.workflowPythonVenvMount && !infra.workflowNodeModulesMount)
|
|
1199
|
+
return;
|
|
1200
|
+
// Runtime provisioning installs through the MITM registry proxy, which is
|
|
1201
|
+
// only wired when packageInstall is enabled (see prepareDockerInfrastructure
|
|
1202
|
+
// — `registries`/`packageValidation` are left undefined otherwise). The
|
|
1203
|
+
// mounts above only exist when the workflow actually ships a
|
|
1204
|
+
// requirements.txt / package.json, so reaching here with package install
|
|
1205
|
+
// disabled means the run genuinely needs deps it can never fetch under
|
|
1206
|
+
// `--network=none`. Fail fast with an actionable message rather than letting
|
|
1207
|
+
// `uv pip install` / `npm install` die with an opaque network error.
|
|
1208
|
+
if (!packageInstallEnabled) {
|
|
1209
|
+
throw new Error('This workflow requires installing dependencies at runtime ' +
|
|
1210
|
+
'(a requirements.txt and/or package.json is present in its scripts), ' +
|
|
1211
|
+
'but packageInstall is disabled. Enable packageInstall in your IronCurtain ' +
|
|
1212
|
+
'config to run workflows that declare runtime dependencies.');
|
|
1213
|
+
}
|
|
1214
|
+
if (infra.workflowPythonVenvMount) {
|
|
1215
|
+
await provisionWorkflowPythonDependencies(infra);
|
|
1216
|
+
}
|
|
1217
|
+
if (infra.workflowNodeModulesMount) {
|
|
1218
|
+
await provisionWorkflowNodeDependencies(infra);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function provisionWorkflowPythonDependencies(infra) {
|
|
1222
|
+
const mount = infra.workflowPythonVenvMount;
|
|
1223
|
+
if (!mount)
|
|
1224
|
+
return;
|
|
1225
|
+
const sentinel = `${mount.target}/.ironcurtain-provisioned-${mount.cacheKey}`;
|
|
1226
|
+
const command = [
|
|
1227
|
+
'set -eu',
|
|
1228
|
+
`if [ -f ${quote([sentinel])} ]; then exit 0; fi`,
|
|
1229
|
+
`find ${quote([mount.target])} -mindepth 1 -maxdepth 1 -exec rm -rf {} +`,
|
|
1230
|
+
`UV_NATIVE_TLS=1 uv venv ${quote([mount.target])}`,
|
|
1231
|
+
`VIRTUAL_ENV=${quote([mount.target])} UV_NATIVE_TLS=1 uv pip install -r ${quote([`${CONTAINER_SCRIPTS_DIR}/requirements.txt`])}`,
|
|
1232
|
+
`touch ${quote([sentinel])}`,
|
|
1233
|
+
].join('\n');
|
|
1234
|
+
// Serialize concurrent provisioning of this content-keyed cache across runs.
|
|
1235
|
+
// The lock is host-side because in-container flock does not propagate across
|
|
1236
|
+
// containers on a Docker Desktop bind mount; the in-shell sentinel check
|
|
1237
|
+
// provides the short-circuit, the host lock makes the second run wait for the
|
|
1238
|
+
// first so it observes the populated cache instead of racing on the clean.
|
|
1239
|
+
await withProvisionLock(mount.hostDir, async () => {
|
|
1240
|
+
logger.info(`Provisioning workflow Python dependencies into ${mount.target}`);
|
|
1241
|
+
const result = await infra.docker.exec(infra.containerId, ['/bin/sh', '-lc', command], 1_200_000, 'codespace', CONTAINER_WORKSPACE_DIR);
|
|
1242
|
+
if (result.exitCode !== 0) {
|
|
1243
|
+
throw new Error(`Workflow Python dependency provisioning failed: ${result.stderr || result.stdout}`);
|
|
1244
|
+
}
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
async function provisionWorkflowNodeDependencies(infra) {
|
|
1248
|
+
const mount = infra.workflowNodeModulesMount;
|
|
1249
|
+
if (!mount)
|
|
1250
|
+
return;
|
|
1251
|
+
const sentinel = `${mount.target}/.ironcurtain-provisioned-${mount.cacheKey}`;
|
|
1252
|
+
const installCommand = mount.hasPackageLock ? 'npm ci --omit=dev' : 'npm install --omit=dev';
|
|
1253
|
+
const command = [
|
|
1254
|
+
'set -eu',
|
|
1255
|
+
`if [ -f ${quote([sentinel])} ]; then exit 0; fi`,
|
|
1256
|
+
'tmp="$(mktemp -d)"',
|
|
1257
|
+
'cleanup() { rm -rf "$tmp"; }',
|
|
1258
|
+
'trap cleanup EXIT',
|
|
1259
|
+
`cp ${quote([`${CONTAINER_SCRIPTS_DIR}/package.json`])} "$tmp/package.json"`,
|
|
1260
|
+
mount.hasPackageLock ? `cp ${quote([`${CONTAINER_SCRIPTS_DIR}/package-lock.json`])} "$tmp/package-lock.json"` : '',
|
|
1261
|
+
'cd "$tmp"',
|
|
1262
|
+
installCommand,
|
|
1263
|
+
`find ${quote([mount.target])} -mindepth 1 -maxdepth 1 -exec rm -rf {} +`,
|
|
1264
|
+
`cp -a node_modules/. ${quote([mount.target])}/`,
|
|
1265
|
+
`touch ${quote([sentinel])}`,
|
|
1266
|
+
]
|
|
1267
|
+
.filter(Boolean)
|
|
1268
|
+
.join('\n');
|
|
1269
|
+
// Host-side serialization of this content-keyed cache (see the Python path
|
|
1270
|
+
// for the bind-mount-flock rationale).
|
|
1271
|
+
await withProvisionLock(mount.hostDir, async () => {
|
|
1272
|
+
logger.info(`Provisioning workflow Node dependencies into ${mount.target}`);
|
|
1273
|
+
const result = await infra.docker.exec(infra.containerId, ['/bin/sh', '-lc', command], 1_200_000, 'codespace', CONTAINER_WORKSPACE_DIR);
|
|
1274
|
+
if (result.exitCode !== 0) {
|
|
1275
|
+
throw new Error(`Workflow Node dependency provisioning failed: ${result.stderr || result.stdout}`);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
822
1278
|
}
|
|
823
1279
|
async function ensureBaseImage(baseImage, docker, ca, dockerDir, dockerfile, buildHash) {
|
|
824
1280
|
if (!(await isImageStale(baseImage, docker, buildHash)))
|
|
825
1281
|
return false;
|
|
826
1282
|
logger.info('Building base Docker image (this may take a while on first run)...');
|
|
827
|
-
|
|
828
|
-
try {
|
|
829
|
-
for (const file of readdirSync(dockerDir)) {
|
|
830
|
-
copyFileSync(resolve(dockerDir, file), resolve(tmpContext, file));
|
|
831
|
-
}
|
|
832
|
-
copyFileSync(ca.certPath, resolve(tmpContext, 'ironcurtain-ca-cert.pem'));
|
|
833
|
-
await docker.buildImage(baseImage, resolve(tmpContext, dockerfile), tmpContext, {
|
|
834
|
-
'ironcurtain.build-hash': buildHash,
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
finally {
|
|
838
|
-
rmSync(tmpContext, { recursive: true, force: true });
|
|
839
|
-
}
|
|
1283
|
+
await buildImageFromCleanContext(docker, baseImage, dockerDir, dockerfile, { 'ironcurtain.build-hash': buildHash }, { 'ironcurtain-ca-cert.pem': ca.certPath });
|
|
840
1284
|
logger.info('Base Docker image built successfully');
|
|
841
1285
|
return true;
|
|
842
1286
|
}
|