@provos/ironcurtain 0.12.0 → 0.13.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 +14 -12
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/config-command.d.ts +33 -1
- package/dist/config/config-command.js +608 -11
- package/dist/config/config-command.js.map +1 -1
- package/dist/config/openrouter-catalog.d.ts +46 -0
- package/dist/config/openrouter-catalog.js +111 -0
- package/dist/config/openrouter-catalog.js.map +1 -0
- package/dist/config/openrouter-models-fallback.d.ts +15 -0
- package/dist/config/openrouter-models-fallback.js +49 -0
- package/dist/config/openrouter-models-fallback.js.map +1 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/config/user-config.d.ts +133 -7
- package/dist/config/user-config.js +234 -5
- package/dist/config/user-config.js.map +1 -1
- package/dist/daemon/daemon-command.js.map +1 -1
- package/dist/daemon/ironcurtain-daemon.js +6 -0
- package/dist/daemon/ironcurtain-daemon.js.map +1 -1
- package/dist/docker/adapters/claude-code.js +109 -1
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/adapters/codex.d.ts +10 -1
- package/dist/docker/adapters/codex.js +99 -20
- package/dist/docker/adapters/codex.js.map +1 -1
- package/dist/docker/adapters/goose.js +38 -2
- package/dist/docker/adapters/goose.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +29 -5
- package/dist/docker/agent-registry.js +1 -1
- package/dist/docker/agent-registry.js.map +1 -1
- package/dist/docker/apple-container-manager.d.ts +4 -4
- package/dist/docker/apple-container-manager.js +44 -22
- package/dist/docker/apple-container-manager.js.map +1 -1
- package/dist/docker/ca.js +9 -2
- package/dist/docker/ca.js.map +1 -1
- package/dist/docker/container-lifecycle.js +9 -0
- package/dist/docker/container-lifecycle.js.map +1 -1
- package/dist/docker/docker-agent-session.d.ts +31 -0
- package/dist/docker/docker-agent-session.js +67 -6
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-infrastructure.d.ts +55 -15
- package/dist/docker/docker-infrastructure.js +261 -61
- package/dist/docker/docker-infrastructure.js.map +1 -1
- package/dist/docker/docker-manager.js +127 -0
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/docker-resource-lifecycle.d.ts +71 -0
- package/dist/docker/docker-resource-lifecycle.js +454 -0
- package/dist/docker/docker-resource-lifecycle.js.map +1 -0
- package/dist/docker/gc-command.d.ts +2 -0
- package/dist/docker/gc-command.js +37 -0
- package/dist/docker/gc-command.js.map +1 -0
- package/dist/docker/mitm-proxy.d.ts +6 -4
- package/dist/docker/mitm-proxy.js +105 -17
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/network-topology.d.ts +16 -9
- package/dist/docker/network-topology.js +17 -10
- package/dist/docker/network-topology.js.map +1 -1
- package/dist/docker/openrouter.d.ts +107 -0
- package/dist/docker/openrouter.js +236 -0
- package/dist/docker/openrouter.js.map +1 -0
- package/dist/docker/orientation.js +1 -1
- package/dist/docker/orientation.js.map +1 -1
- package/dist/docker/platform.d.ts +3 -1
- package/dist/docker/platform.js +3 -1
- package/dist/docker/platform.js.map +1 -1
- package/dist/docker/provider-config.d.ts +6 -0
- package/dist/docker/provider-config.js.map +1 -1
- package/dist/docker/pty-session.d.ts +7 -0
- package/dist/docker/pty-session.js +111 -29
- package/dist/docker/pty-session.js.map +1 -1
- package/dist/docker/pty-types.d.ts +15 -0
- package/dist/docker/pty-types.js +8 -0
- package/dist/docker/pty-types.js.map +1 -1
- package/dist/docker/sse-extractor.js +55 -5
- package/dist/docker/sse-extractor.js.map +1 -1
- package/dist/docker/stream-delay.d.ts +86 -0
- package/dist/docker/stream-delay.js +218 -0
- package/dist/docker/stream-delay.js.map +1 -0
- package/dist/docker/token-stream-types.d.ts +15 -0
- package/dist/docker/trajectory-reassembler.d.ts +2 -2
- package/dist/docker/trajectory-reassembler.js +22 -2
- package/dist/docker/trajectory-reassembler.js.map +1 -1
- package/dist/docker/trajectory-tap.js +2 -2
- package/dist/docker/trajectory-tap.js.map +1 -1
- package/dist/docker/types.d.ts +63 -5
- package/dist/doctor/checks.d.ts +3 -3
- package/dist/doctor/checks.js +28 -11
- package/dist/doctor/checks.js.map +1 -1
- package/dist/doctor/doctor-command.js +1 -1
- package/dist/doctor/doctor-command.js.map +1 -1
- package/dist/escalation/escalation-watcher.js +6 -0
- package/dist/escalation/escalation-watcher.js.map +1 -1
- package/dist/{mux → escalation}/trusted-input.d.ts +7 -4
- package/dist/{mux → escalation}/trusted-input.js +8 -5
- package/dist/escalation/trusted-input.js.map +1 -0
- package/dist/hash.js.map +1 -1
- package/dist/index.js +37 -6
- package/dist/index.js.map +1 -1
- package/dist/mux/mux-app.d.ts +7 -0
- package/dist/mux/mux-app.js +17 -16
- package/dist/mux/mux-app.js.map +1 -1
- package/dist/mux/mux-command.js +8 -3
- package/dist/mux/mux-command.js.map +1 -1
- package/dist/mux/mux-input-handler.d.ts +26 -2
- package/dist/mux/mux-input-handler.js +93 -11
- package/dist/mux/mux-input-handler.js.map +1 -1
- package/dist/mux/mux-renderer.d.ts +2 -1
- package/dist/mux/mux-renderer.js +67 -0
- package/dist/mux/mux-renderer.js.map +1 -1
- package/dist/mux/provider-profile-snapshot.d.ts +44 -0
- package/dist/mux/provider-profile-snapshot.js +56 -0
- package/dist/mux/provider-profile-snapshot.js.map +1 -0
- package/dist/mux/types.d.ts +3 -2
- package/dist/mux/types.js +6 -2
- package/dist/mux/types.js.map +1 -1
- package/dist/observe/observe-command.js.map +1 -1
- package/dist/pipeline/constitution-compiler.js.map +1 -1
- package/dist/pipeline/generate-with-repair.js.map +1 -1
- package/dist/pipeline/refresh-lists.js.map +1 -1
- package/dist/pipeline/scenario-schema-validator.js.map +1 -1
- package/dist/{mux → pty}/pty-bridge.d.ts +39 -3
- package/dist/{mux → pty}/pty-bridge.js +68 -18
- package/dist/pty/pty-bridge.js.map +1 -0
- package/dist/pty/resolve-ironcurtain-bin.d.ts +17 -0
- package/dist/pty/resolve-ironcurtain-bin.js +22 -0
- package/dist/pty/resolve-ironcurtain-bin.js.map +1 -0
- package/dist/servers/fetch-server.js +2 -0
- package/dist/servers/fetch-server.js.map +1 -1
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +11 -2
- package/dist/session/index.js.map +1 -1
- package/dist/session/preflight.d.ts +19 -5
- package/dist/session/preflight.js +41 -25
- package/dist/session/preflight.js.map +1 -1
- package/dist/session/resource-budget-tracker.js +8 -0
- package/dist/session/resource-budget-tracker.js.map +1 -1
- package/dist/session/session-manager.d.ts +7 -0
- package/dist/session/session-manager.js.map +1 -1
- package/dist/session/types.d.ts +14 -0
- package/dist/signal/signal-bot-daemon.d.ts +1 -0
- package/dist/signal/signal-bot-daemon.js +9 -2
- package/dist/signal/signal-bot-daemon.js.map +1 -1
- package/dist/trusted-process/mcp-error-utils.js +4 -1
- package/dist/trusted-process/mcp-error-utils.js.map +1 -1
- package/dist/trusted-process/mcp-proxy-server.js +2 -0
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/tcp-server-transport.d.ts +7 -0
- package/dist/trusted-process/tcp-server-transport.js +48 -10
- package/dist/trusted-process/tcp-server-transport.js.map +1 -1
- package/dist/utils/preflight-checks.js +1 -3
- package/dist/utils/preflight-checks.js.map +1 -1
- package/dist/utils/stdio-guard.d.ts +1 -0
- package/dist/utils/stdio-guard.js +27 -0
- package/dist/utils/stdio-guard.js.map +1 -0
- package/dist/web-ui/__tests__/config-dispatch.test.d.ts +13 -0
- package/dist/web-ui/__tests__/config-dispatch.test.js +383 -0
- package/dist/web-ui/__tests__/config-dispatch.test.js.map +1 -0
- package/dist/web-ui/__tests__/json-rpc-dispatch.test.js +6 -0
- package/dist/web-ui/__tests__/json-rpc-dispatch.test.js.map +1 -1
- package/dist/web-ui/dispatch/config-dispatch.d.ts +15 -0
- package/dist/web-ui/dispatch/config-dispatch.js +277 -0
- package/dist/web-ui/dispatch/config-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/escalation-dispatch.js +11 -2
- package/dist/web-ui/dispatch/escalation-dispatch.js.map +1 -1
- package/dist/web-ui/dispatch/pty-dispatch.d.ts +15 -0
- package/dist/web-ui/dispatch/pty-dispatch.js +66 -0
- package/dist/web-ui/dispatch/pty-dispatch.js.map +1 -0
- package/dist/web-ui/dispatch/session-dispatch.js +69 -4
- package/dist/web-ui/dispatch/session-dispatch.js.map +1 -1
- package/dist/web-ui/dispatch/types.d.ts +12 -0
- package/dist/web-ui/dispatch/types.js +38 -1
- package/dist/web-ui/dispatch/types.js.map +1 -1
- package/dist/web-ui/dispatch/workflow-dispatch.js.map +1 -1
- package/dist/web-ui/json-rpc-dispatch.js +3 -0
- package/dist/web-ui/json-rpc-dispatch.js.map +1 -1
- package/dist/web-ui/pty-session-manager.d.ts +234 -0
- package/dist/web-ui/pty-session-manager.js +599 -0
- package/dist/web-ui/pty-session-manager.js.map +1 -0
- package/dist/web-ui/web-event-bus.d.ts +9 -0
- package/dist/web-ui/web-event-bus.js.map +1 -1
- package/dist/web-ui/web-session-transport.js.map +1 -1
- package/dist/web-ui/web-ui-server.d.ts +8 -0
- package/dist/web-ui/web-ui-server.js +25 -0
- package/dist/web-ui/web-ui-server.js.map +1 -1
- package/dist/web-ui/web-ui-types.d.ts +80 -1
- package/dist/web-ui/web-ui-types.js.map +1 -1
- package/dist/web-ui-static/assets/index-CCVI4nEK.css +1 -0
- package/dist/web-ui-static/assets/index-Dm012gvY.js +114 -0
- package/dist/web-ui-static/index.html +2 -2
- package/dist/workflow/daemon-gate-commands.js +38 -42
- package/dist/workflow/daemon-gate-commands.js.map +1 -1
- package/dist/workflow/machine-builder.js.map +1 -1
- package/dist/workflow/orchestrator.js +10 -7
- package/dist/workflow/orchestrator.js.map +1 -1
- package/dist/workflow/shutdown-signals.d.ts +7 -0
- package/dist/workflow/shutdown-signals.js +40 -0
- package/dist/workflow/shutdown-signals.js.map +1 -0
- package/dist/workflow/validate.js.map +1 -1
- package/dist/workflow/workflow-command.js +10 -8
- package/dist/workflow/workflow-command.js.map +1 -1
- package/dist/workflow/workflows/evolve/README.md +66 -0
- package/dist/workflow/workflows/vuln-discovery/README.md +54 -0
- package/docker/entrypoint-claude-code.sh +22 -0
- package/package.json +31 -27
- package/src/workflow/workflows/evolve/README.md +66 -0
- package/src/workflow/workflows/vuln-discovery/README.md +54 -0
- package/dist/mux/pty-bridge.js.map +0 -1
- package/dist/mux/trusted-input.js.map +0 -1
- package/dist/web-ui-static/assets/index-BD5U6ILh.css +0 -1
- package/dist/web-ui-static/assets/index-FMsHWEfK.js +0 -79
- package/packages/web-ui/README.md +0 -113
|
@@ -15,14 +15,17 @@ 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
17
|
import { CONTAINER_SCRIPTS_DIR, CONTAINER_WORKSPACE_DIR, } from './agent-adapter.js';
|
|
18
|
+
import { OPENROUTER_HOST, resolveActiveProfile } from '../config/user-config.js';
|
|
18
19
|
import { parseUpstreamBaseUrl } from './provider-config.js';
|
|
19
20
|
import { getInternalNetworkName } from './platform.js';
|
|
20
21
|
import { cleanupContainers } from './container-lifecycle.js';
|
|
22
|
+
import { createIronCurtainInternalNetwork, InternalNetworkConnectivityError, managedResourceLabels, reconcileIronCurtainDockerResourcesBestEffort, releaseManagedResourceLease, withInternalNetworkAllocationRetry, } from './docker-resource-lifecycle.js';
|
|
21
23
|
import { clampDockerResources } from './resource-limits.js';
|
|
22
24
|
import { errorMessage } from '../utils/error-message.js';
|
|
23
25
|
import { createCachedStager } from '../skills/staging.js';
|
|
24
26
|
import { withProvisionLock } from './provision-lock.js';
|
|
25
27
|
import * as logger from '../logger.js';
|
|
28
|
+
export { InternalNetworkConnectivityError };
|
|
26
29
|
/**
|
|
27
30
|
* Create a bundle-owned directory and enforce 0o700 permissions even if
|
|
28
31
|
* it already exists. `mkdirSync`'s `mode` only applies on creation, so a
|
|
@@ -77,7 +80,24 @@ const BUNDLE_SKILLS_SUBDIR = 'skills';
|
|
|
77
80
|
const WORKFLOW_PYTHON_VENV_DIR = '/opt/workflow-venv';
|
|
78
81
|
/** Container path for runtime-provisioned workflow Node dependencies. */
|
|
79
82
|
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) {
|
|
83
|
+
export async function prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir, providerProfileName) {
|
|
84
|
+
// Resolve and STAMP the active provider profile as the FIRST step (§9.7 F1),
|
|
85
|
+
// before any container-runtime probe, adapter registration, or auth
|
|
86
|
+
// detection. This ordering is load-bearing: Claude Code's
|
|
87
|
+
// detectCredential(config) reads config.activeProviderProfile to return an
|
|
88
|
+
// API-key AuthMethod for an OpenRouter-only user, so the stamp must already
|
|
89
|
+
// be present when auth detection runs below. An unknown providerProfileName
|
|
90
|
+
// throws a clear error listing the available profiles before any expensive
|
|
91
|
+
// work or container launch. Safe to mutate: callers always pass a
|
|
92
|
+
// session-specific config copy (the same invariant the config.dockerAuth
|
|
93
|
+
// stamp below relies on).
|
|
94
|
+
const activeProfile = resolveActiveProfile(config.userConfig.modelProviders, providerProfileName);
|
|
95
|
+
config.activeProviderProfile = activeProfile;
|
|
96
|
+
if (activeProfile.type === 'openrouter' && activeProfile.apiKey === '') {
|
|
97
|
+
const activeName = providerProfileName ?? config.userConfig.modelProviders.default;
|
|
98
|
+
throw new Error(`Provider profile "${activeName}" is OpenRouter but no API key is configured. ` +
|
|
99
|
+
"Set OPENROUTER_API_KEY or the profile's apiKey in ~/.ironcurtain/config.json.");
|
|
100
|
+
}
|
|
81
101
|
// The audit log path is read from config so the bundle is
|
|
82
102
|
// self-describing: downstream consumers (AuditLogTailer, sandbox
|
|
83
103
|
// coordinator) can take it from either `config.auditLogPath` or
|
|
@@ -102,9 +122,14 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
102
122
|
const topology = resolveNetworkTopology(runtimeKind);
|
|
103
123
|
const useTcp = topology !== 'uds';
|
|
104
124
|
// Detect authentication method. Adapters with detectCredential() handle
|
|
105
|
-
// their own credential detection (e.g., Goose checks provider-specific keys
|
|
106
|
-
//
|
|
107
|
-
|
|
125
|
+
// their own credential detection (e.g., Goose checks provider-specific keys;
|
|
126
|
+
// Claude Code returns an api-key AuthMethod for an OpenRouter-only profile).
|
|
127
|
+
// A `detectCredential` that returns `undefined` DEFERS to detectAuthMethod()
|
|
128
|
+
// (Anthropic OAuth + API key) — this is how Claude Code preserves today's
|
|
129
|
+
// detection for a native profile (B2a). Adapters without the method also
|
|
130
|
+
// fall back.
|
|
131
|
+
const detected = adapter.detectCredential?.(config);
|
|
132
|
+
const authMethod = detected ?? (await detectAuthMethod(config));
|
|
108
133
|
if (authMethod.kind === 'none') {
|
|
109
134
|
throw new Error(adapter.credentialHelpText ??
|
|
110
135
|
'No credentials available for Docker session. ' +
|
|
@@ -181,7 +206,7 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
181
206
|
...tokenManagerCodexDeps,
|
|
182
207
|
})
|
|
183
208
|
: undefined;
|
|
184
|
-
const providers = adapter.getProviders(authKind);
|
|
209
|
+
const providers = adapter.getProviders(config, authKind);
|
|
185
210
|
const resolvedProviders = applyUpstreamOverrides(providers, parseUpstreamBaseUrl, {
|
|
186
211
|
'api.anthropic.com': config.userConfig.anthropicBaseUrl,
|
|
187
212
|
'api.openai.com': config.userConfig.openaiBaseUrl,
|
|
@@ -287,6 +312,14 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
287
312
|
else {
|
|
288
313
|
logger.info(`MITM proxy listening on ${mitmAddr.socketPath}`);
|
|
289
314
|
}
|
|
315
|
+
// apple-container's `-v <sock>` vsock relay propagates the host
|
|
316
|
+
// socket's mode bits to the guest side (owner is always root there),
|
|
317
|
+
// so the non-root `codespace` user can only connect() when "other"
|
|
318
|
+
// has write. The parent `sockets/` dir is 0o700, so widening the
|
|
319
|
+
// socket mode does not expose it to other host users.
|
|
320
|
+
if (!useTcp && runtimeKind === 'apple-container' && mitmAddr.socketPath !== undefined) {
|
|
321
|
+
chmodSync(mitmAddr.socketPath, 0o666);
|
|
322
|
+
}
|
|
290
323
|
// Compute control address for the proxy tools MCP server instance
|
|
291
324
|
const controlAddr = mitmAddr.controlPort !== undefined
|
|
292
325
|
? `http://127.0.0.1:${mitmAddr.controlPort}`
|
|
@@ -308,6 +341,9 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
308
341
|
else {
|
|
309
342
|
logger.info(`Code Mode proxy listening on ${proxy.socketPath}`);
|
|
310
343
|
}
|
|
344
|
+
if (!useTcp && runtimeKind === 'apple-container') {
|
|
345
|
+
chmodSync(socketPath, 0o666);
|
|
346
|
+
}
|
|
311
347
|
// Remaining setup steps can fail -- clean up started proxies on error.
|
|
312
348
|
try {
|
|
313
349
|
// Build orientation
|
|
@@ -337,6 +373,10 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
337
373
|
const agentImage = await adapter.getImage();
|
|
338
374
|
const agentBuildHash = await ensureImage(agentImage, docker, ca);
|
|
339
375
|
const image = agentImage;
|
|
376
|
+
// Surface the (unpinned) agent CLI version baked into the image so silent
|
|
377
|
+
// version drift on rebuild is visible in logs (issue #367). Keyed by build
|
|
378
|
+
// hash so a same-process rebuild re-logs the possibly-changed version.
|
|
379
|
+
await logResolvedAgentVersion(docker, agentImage, agentBuildHash, adapter.versionProbe);
|
|
340
380
|
const workflowDependencyMounts = prepareWorkflowDependencyMounts(agentBuildHash, scriptsDir, getIronCurtainHome());
|
|
341
381
|
const orientationDir = resolve(bundleDir, 'orientation');
|
|
342
382
|
// Set up conversation state directory if the adapter supports resume
|
|
@@ -458,8 +498,8 @@ export async function prepareDockerInfrastructure(config, mode, bundleDir, works
|
|
|
458
498
|
* failure after the proxies are started, all started resources are torn
|
|
459
499
|
* down before the error propagates.
|
|
460
500
|
*/
|
|
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);
|
|
501
|
+
export async function createDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir, options, providerProfileName) {
|
|
502
|
+
const core = await prepareDockerInfrastructure(config, mode, bundleDir, workspaceDir, escalationDir, bundleId, workflowId, scope, resolvedSkills, captureInput, scriptsDir, providerProfileName);
|
|
463
503
|
let containerResources;
|
|
464
504
|
try {
|
|
465
505
|
containerResources = await createSessionContainers(core, config, options);
|
|
@@ -509,6 +549,8 @@ export async function destroyDockerInfrastructure(infra) {
|
|
|
509
549
|
sidecarContainerId: infra.sidecarContainerId ?? null,
|
|
510
550
|
networkName: infra.internalNetwork ?? null,
|
|
511
551
|
});
|
|
552
|
+
if (infra.runtimeKind === 'docker')
|
|
553
|
+
releaseManagedResourceLease(infra.bundleId);
|
|
512
554
|
// Proxies are independent producers -- stop them in parallel. Each
|
|
513
555
|
// per-promise catch logs so one failure doesn't mask the other, and
|
|
514
556
|
// allSettled ensures both complete even if one throws synchronously.
|
|
@@ -561,6 +603,10 @@ export async function destroyDockerInfrastructure(infra) {
|
|
|
561
603
|
* See `docs/designs/workflow-session-identity.md` §7.
|
|
562
604
|
*/
|
|
563
605
|
export function buildBundleLabels(core) {
|
|
606
|
+
const managedLabels = core.runtimeKind === 'docker' ? managedResourceLabels(core.bundleId) : undefined;
|
|
607
|
+
const labels = managedLabels
|
|
608
|
+
? Object.fromEntries(Object.entries(managedLabels).filter(([key]) => key !== 'ironcurtain.bundle'))
|
|
609
|
+
: undefined;
|
|
564
610
|
if (core.workflowId !== undefined) {
|
|
565
611
|
return {
|
|
566
612
|
bundleLabel: core.bundleId,
|
|
@@ -569,9 +615,10 @@ export function buildBundleLabels(core) {
|
|
|
569
615
|
// bundle; default-fall back to DEFAULT_CONTAINER_SCOPE so that a
|
|
570
616
|
// workflow bundle always carries a scope label.
|
|
571
617
|
scopeLabel: core.scope ?? DEFAULT_CONTAINER_SCOPE,
|
|
618
|
+
labels,
|
|
572
619
|
};
|
|
573
620
|
}
|
|
574
|
-
return { bundleLabel: core.bundleId };
|
|
621
|
+
return { bundleLabel: core.bundleId, labels };
|
|
575
622
|
}
|
|
576
623
|
/**
|
|
577
624
|
* Returns the `user` override and env vars needed for runtime UID
|
|
@@ -583,15 +630,20 @@ export function buildBundleLabels(core) {
|
|
|
583
630
|
* so the entrypoint (running as root) can renumber the codespace
|
|
584
631
|
* account before dropping privileges via `runuser`.
|
|
585
632
|
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
633
|
+
* When `skipRemap` is true this returns an empty mapping so the
|
|
634
|
+
* container runs as the baked `codespace` user from the Dockerfile.
|
|
635
|
+
* Skipped on macOS Docker Desktop (VirtioFS handles UID translation
|
|
636
|
+
* transparently and `--user 0:0` would defeat it) AND on apple-container
|
|
637
|
+
* (virtiofs presents host files as root; the existing sessions work
|
|
638
|
+
* without the Linux renumber-and-drop dance) — i.e., remap is Linux
|
|
639
|
+
* Docker only. Callers pass `runtimeKind === 'apple-container' || useTcp`
|
|
640
|
+
* so apple-container's `uds` topology does not accidentally trip the
|
|
641
|
+
* Linux-only remap.
|
|
590
642
|
*
|
|
591
643
|
* Exported for testability.
|
|
592
644
|
*/
|
|
593
|
-
export function buildAgentUidRemap(
|
|
594
|
-
if (
|
|
645
|
+
export function buildAgentUidRemap(skipRemap) {
|
|
646
|
+
if (skipRemap)
|
|
595
647
|
return { user: undefined, env: {} };
|
|
596
648
|
const uid = process.getuid?.() ?? 1000;
|
|
597
649
|
const gid = process.getgid?.() ?? 1000;
|
|
@@ -603,6 +655,30 @@ export function buildAgentUidRemap(useTcp) {
|
|
|
603
655
|
},
|
|
604
656
|
};
|
|
605
657
|
}
|
|
658
|
+
/** In-container mount root for the UDS proxies (both runtimes). */
|
|
659
|
+
export const CONTAINER_SOCKETS_DIR = '/run/ironcurtain';
|
|
660
|
+
/**
|
|
661
|
+
* Returns the `uds`-topology socket mounts for a given runtime. Linux
|
|
662
|
+
* Docker bind-mounts the whole `sockets/` directory (the sockets are
|
|
663
|
+
* shared kernel objects). Apple `container` mounts each socket FILE via
|
|
664
|
+
* `-v` so the runtime creates a per-socket vsock relay — a virtiofs
|
|
665
|
+
* directory share does not carry sockets (verified: `connect()` on a
|
|
666
|
+
* socket inside a shared dir fails ENOTSUP on 1.1.0). Shared by batch
|
|
667
|
+
* (`createSessionContainers`) and PTY (`runPtySession`).
|
|
668
|
+
*/
|
|
669
|
+
export function buildUdsSocketMounts(runtimeKind, socketsDir) {
|
|
670
|
+
if (runtimeKind === 'apple-container') {
|
|
671
|
+
return [
|
|
672
|
+
{ source: resolve(socketsDir, 'proxy.sock'), target: `${CONTAINER_SOCKETS_DIR}/proxy.sock`, readonly: false },
|
|
673
|
+
{
|
|
674
|
+
source: resolve(socketsDir, 'mitm-proxy.sock'),
|
|
675
|
+
target: `${CONTAINER_SOCKETS_DIR}/mitm-proxy.sock`,
|
|
676
|
+
readonly: false,
|
|
677
|
+
},
|
|
678
|
+
];
|
|
679
|
+
}
|
|
680
|
+
return [{ source: socketsDir, target: CONTAINER_SOCKETS_DIR, readonly: false }];
|
|
681
|
+
}
|
|
606
682
|
/**
|
|
607
683
|
* Creates and starts the main agent container (plus TCP-mode sidecar and
|
|
608
684
|
* internal network on macOS). Cleans up any partially-created resources
|
|
@@ -613,6 +689,17 @@ export function buildAgentUidRemap(useTcp) {
|
|
|
613
689
|
* with a scripted `ContainerRuntime`.
|
|
614
690
|
*/
|
|
615
691
|
export async function createSessionContainers(core, config, options) {
|
|
692
|
+
if (core.runtimeKind === 'docker') {
|
|
693
|
+
await reconcileIronCurtainDockerResourcesBestEffort(core.docker, 'session startup');
|
|
694
|
+
}
|
|
695
|
+
const maxAttempts = core.topology === 'tcp-sidecar' ? 4 : 1;
|
|
696
|
+
return withInternalNetworkAllocationRetry({
|
|
697
|
+
maxAttempts,
|
|
698
|
+
description: 'Internal Docker network',
|
|
699
|
+
reconcile: () => reconcileIronCurtainDockerResourcesBestEffort(core.docker, 'internal network retry'),
|
|
700
|
+
}, (excludedSubnets, attempt) => createSessionContainersAttempt(core, config, options, excludedSubnets, attempt));
|
|
701
|
+
}
|
|
702
|
+
async function createSessionContainersAttempt(core, config, options, excludedSubnets, attempt) {
|
|
616
703
|
const shortId = getBundleShortId(core.bundleId);
|
|
617
704
|
const mainContainerName = `${CONTAINER_NAME_PREFIX}${shortId}`;
|
|
618
705
|
// Labels applied to every IronCurtain-owned container (main + sidecar).
|
|
@@ -628,6 +715,7 @@ export async function createSessionContainers(core, config, options) {
|
|
|
628
715
|
let mainContainerId;
|
|
629
716
|
let sidecarContainerId;
|
|
630
717
|
let internalNetwork;
|
|
718
|
+
let allocatedNetworkSubnet;
|
|
631
719
|
try {
|
|
632
720
|
const mainImage = options?.baseImageOverride && (await core.docker.imageExists(options.baseImageOverride))
|
|
633
721
|
? options.baseImageOverride
|
|
@@ -644,12 +732,11 @@ export async function createSessionContainers(core, config, options) {
|
|
|
644
732
|
};
|
|
645
733
|
let network;
|
|
646
734
|
let extraHosts;
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
|
|
652
|
-
let hostOnlyAptProxyUrl;
|
|
735
|
+
// apple-container only (both `uds` and the retained `tcp-hostonly`):
|
|
736
|
+
// the apt proxy config is written into the container via exec after
|
|
737
|
+
// start instead of bind-mounted — see writeAptProxyConfigViaExec for
|
|
738
|
+
// the virtiofs constraints that make the bind mount unworkable.
|
|
739
|
+
let execAptProxyUrl;
|
|
653
740
|
if (core.topology === 'tcp-hostonly') {
|
|
654
741
|
// Apple container host-only mode: the agent VM reaches the host
|
|
655
742
|
// proxies directly at the vmnet gateway address. No sidecar, no
|
|
@@ -664,7 +751,7 @@ export async function createSessionContainers(core, config, options) {
|
|
|
664
751
|
HTTPS_PROXY: proxyUrl,
|
|
665
752
|
HTTP_PROXY: proxyUrl,
|
|
666
753
|
};
|
|
667
|
-
|
|
754
|
+
execAptProxyUrl = proxyUrl;
|
|
668
755
|
network = core.hostOnlyNetwork.name;
|
|
669
756
|
// Report the host-only network as `internalNetwork` so the standard
|
|
670
757
|
// teardown paths (destroyDockerInfrastructure, rollback below)
|
|
@@ -688,10 +775,15 @@ export async function createSessionContainers(core, config, options) {
|
|
|
688
775
|
writeFileSync(aptProxyPath, `Acquire::http::Proxy "${proxyUrl}";\nAcquire::https::Proxy "${proxyUrl}";\n`);
|
|
689
776
|
mounts.push({ source: aptProxyPath, target: '/etc/apt/apt.conf.d/90-ironcurtain-proxy', readonly: true });
|
|
690
777
|
// Create a per-session --internal Docker network that blocks internet egress.
|
|
691
|
-
const
|
|
692
|
-
|
|
778
|
+
const baseNetworkName = getInternalNetworkName(shortId);
|
|
779
|
+
const networkName = attempt === 1 ? baseNetworkName : `${baseNetworkName}-a${attempt}`;
|
|
780
|
+
const allocatedNetwork = await createIronCurtainInternalNetwork(core.docker, networkName, core.bundleId, {
|
|
781
|
+
excludedSubnets,
|
|
782
|
+
});
|
|
783
|
+
allocatedNetworkSubnet = allocatedNetwork.subnet;
|
|
693
784
|
internalNetwork = networkName;
|
|
694
785
|
network = networkName;
|
|
786
|
+
logger.info(`Allocated internal Docker network ${networkName} at ${allocatedNetwork.subnet}`);
|
|
695
787
|
// Ensure the socat image is available
|
|
696
788
|
const socatImage = 'alpine/socat';
|
|
697
789
|
if (!(await core.docker.imageExists(socatImage))) {
|
|
@@ -726,30 +818,43 @@ export async function createSessionContainers(core, config, options) {
|
|
|
726
818
|
logger.info(`Sidecar ${sidecarName} bridging ports ${mcpPort},${mitmPort} at ${sidecarIp}`);
|
|
727
819
|
}
|
|
728
820
|
else {
|
|
729
|
-
//
|
|
730
|
-
|
|
821
|
+
// UDS mode (Linux Docker AND apple-container >= 1.1.0):
|
|
822
|
+
// --network none, proxy sockets mounted at /run/ironcurtain.
|
|
823
|
+
const udsProxyUrl = 'http://127.0.0.1:18080';
|
|
731
824
|
env = {
|
|
732
825
|
...env,
|
|
733
|
-
HTTPS_PROXY:
|
|
734
|
-
HTTP_PROXY:
|
|
826
|
+
HTTPS_PROXY: udsProxyUrl,
|
|
827
|
+
HTTP_PROXY: udsProxyUrl,
|
|
735
828
|
};
|
|
736
829
|
network = null;
|
|
737
|
-
// Write apt proxy config so sudo apt-get routes through the MITM
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
830
|
+
// Write apt proxy config so sudo apt-get routes through the MITM
|
|
831
|
+
// proxy. On apple-container we CANNOT bind-mount this file: it
|
|
832
|
+
// lives inside `orientationDir`, and 1.1.0's virtiofs silently
|
|
833
|
+
// drops a directory share when a file inside that directory is
|
|
834
|
+
// ALSO `-v`-mounted (verified — /etc/ironcurtain vanishes). The
|
|
835
|
+
// exec-based writer runs after start instead. Linux Docker keeps
|
|
836
|
+
// the single-file bind mount.
|
|
837
|
+
if (core.runtimeKind === 'apple-container') {
|
|
838
|
+
execAptProxyUrl = udsProxyUrl;
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
const aptProxyPathUds = resolve(core.orientationDir, 'apt-proxy.conf');
|
|
842
|
+
writeFileSync(aptProxyPathUds, `Acquire::http::Proxy "${udsProxyUrl}";\nAcquire::https::Proxy "${udsProxyUrl}";\n`);
|
|
843
|
+
mounts.push({
|
|
844
|
+
source: aptProxyPathUds,
|
|
845
|
+
target: '/etc/apt/apt.conf.d/90-ironcurtain-proxy',
|
|
846
|
+
readonly: true,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
// Mount ONLY the per-bundle proxy sockets into the container. The
|
|
850
|
+
// sockets dir lives under a short `~/.ironcurtain/run/<bid12>/`
|
|
851
|
+
// path (see `getBundleSocketsDir`) so host paths stay under
|
|
852
|
+
// `sockaddr_un.sun_path`. Linux mounts the directory; apple-
|
|
853
|
+
// container mounts each socket file so the runtime creates a
|
|
854
|
+
// vsock relay per socket (see buildUdsSocketMounts). The
|
|
855
|
+
// host-only MITM control socket lives in a sibling `host/` dir
|
|
856
|
+
// and is never mounted.
|
|
857
|
+
mounts.push(...buildUdsSocketMounts(core.runtimeKind, core.socketsDir));
|
|
753
858
|
}
|
|
754
859
|
// Mount conversation state directory for session resume (e.g., claude --continue)
|
|
755
860
|
if (core.conversationStateDir && core.conversationStateConfig) {
|
|
@@ -790,7 +895,7 @@ export async function createSessionContainers(core, config, options) {
|
|
|
790
895
|
// passing `--user 0:0` would actually break that translation,
|
|
791
896
|
// so we leave the container running as the baked codespace user
|
|
792
897
|
// and skip the env vars entirely.
|
|
793
|
-
const uidRemap = buildAgentUidRemap(core.useTcp);
|
|
898
|
+
const uidRemap = buildAgentUidRemap(core.runtimeKind === 'apple-container' || core.useTcp);
|
|
794
899
|
// Resource ceilings come from userConfig (defaults: 8 GB / 4 cpus) and
|
|
795
900
|
// are clamped to fit the host. `null` in either field is preserved as
|
|
796
901
|
// "no flag emitted" (see clampDockerResources docs).
|
|
@@ -828,10 +933,13 @@ export async function createSessionContainers(core, config, options) {
|
|
|
828
933
|
});
|
|
829
934
|
await core.docker.start(mainContainerId);
|
|
830
935
|
logger.info(`Container started: ${mainContainerId.substring(0, 12)}`);
|
|
936
|
+
if (core.runtimeKind === 'docker') {
|
|
937
|
+
await checkDockerContainerWritableStorage(core.docker, mainContainerId);
|
|
938
|
+
}
|
|
831
939
|
// tcp-hostonly: write the apt proxy config inside the container (the
|
|
832
|
-
// Docker topologies bind-mount it; see
|
|
833
|
-
if (
|
|
834
|
-
await
|
|
940
|
+
// Docker topologies bind-mount it; see execAptProxyUrl above).
|
|
941
|
+
if (execAptProxyUrl !== undefined) {
|
|
942
|
+
await writeAptProxyConfigViaExec(core.docker, mainContainerId, execAptProxyUrl);
|
|
835
943
|
}
|
|
836
944
|
// Connectivity check: verify the container can reach host proxies
|
|
837
945
|
// through the internal network. Abort if unreachable. Host-only
|
|
@@ -841,7 +949,18 @@ export async function createSessionContainers(core, config, options) {
|
|
|
841
949
|
await checkHostOnlyConnectivity(core.docker, mainContainerId, core.hostOnlyNetwork.gateway, core.proxy.port);
|
|
842
950
|
}
|
|
843
951
|
else if (core.useTcp && internalNetwork !== undefined && core.proxy.port !== undefined) {
|
|
844
|
-
|
|
952
|
+
if (core.mitmAddr.port === undefined) {
|
|
953
|
+
throw new Error('tcp-sidecar bundle is missing its MITM proxy port');
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
await checkInternalNetworkConnectivity(core.docker, mainContainerId, core.proxy.port, core.mitmAddr.port);
|
|
957
|
+
}
|
|
958
|
+
catch (error) {
|
|
959
|
+
if (error instanceof InternalNetworkConnectivityError) {
|
|
960
|
+
throw new InternalNetworkConnectivityError(error.message, allocatedNetworkSubnet);
|
|
961
|
+
}
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
845
964
|
}
|
|
846
965
|
return {
|
|
847
966
|
containerId: mainContainerId,
|
|
@@ -860,6 +979,8 @@ export async function createSessionContainers(core, config, options) {
|
|
|
860
979
|
sidecarContainerId: sidecarContainerId ?? null,
|
|
861
980
|
networkName: internalNetwork ?? null,
|
|
862
981
|
});
|
|
982
|
+
if (core.runtimeKind === 'docker')
|
|
983
|
+
releaseManagedResourceLease(core.bundleId);
|
|
863
984
|
throw err;
|
|
864
985
|
}
|
|
865
986
|
}
|
|
@@ -867,26 +988,60 @@ export async function createSessionContainers(core, config, options) {
|
|
|
867
988
|
* Probes whether the container can reach host-side proxies via the socat
|
|
868
989
|
* sidecar on the internal Docker network. Throws a descriptive error if not.
|
|
869
990
|
*/
|
|
870
|
-
async function checkInternalNetworkConnectivity(docker, containerId, mcpPort) {
|
|
871
|
-
const
|
|
991
|
+
export async function checkInternalNetworkConnectivity(docker, containerId, mcpPort, mitmPort) {
|
|
992
|
+
const mcpResult = await docker.exec(containerId, [
|
|
993
|
+
'/bin/sh',
|
|
994
|
+
'-c',
|
|
995
|
+
`printf 'IRONCURTAIN_HEALTH/1\\n' | socat -T5 - TCP:${DOCKER_HOST_GATEWAY}:${mcpPort},connect-timeout=5 | grep -q '^IRONCURTAIN_OK/1'`,
|
|
996
|
+
],
|
|
872
997
|
// Allow a small buffer above socat's 5s connect-timeout for docker exec/process startup overhead.
|
|
873
998
|
6_000);
|
|
874
|
-
if (
|
|
875
|
-
throw new
|
|
876
|
-
`
|
|
877
|
-
`Check that the sidecar container is running and connected to the internal network.`);
|
|
999
|
+
if (mcpResult.exitCode !== 0) {
|
|
1000
|
+
throw new InternalNetworkConnectivityError(`Internal network connectivity check failed: MCP round-trip exited ${mcpResult.exitCode}; ` +
|
|
1001
|
+
`the sidecar could not reach the host proxy.`);
|
|
878
1002
|
}
|
|
1003
|
+
const healthRequest = 'GET http://ironcurtain.invalid/__ironcurtain/health HTTP/1.1\\r\\n' +
|
|
1004
|
+
'Host: ironcurtain.invalid\\r\\nConnection: close\\r\\n\\r\\n';
|
|
1005
|
+
const mitmResult = await docker.exec(containerId, [
|
|
1006
|
+
'/bin/sh',
|
|
1007
|
+
'-c',
|
|
1008
|
+
`printf '${healthRequest}' | socat -T5 - TCP:${DOCKER_HOST_GATEWAY}:${mitmPort},connect-timeout=5 | grep -q 'IRONCURTAIN_OK/1'`,
|
|
1009
|
+
], 6_000);
|
|
1010
|
+
if (mitmResult.exitCode !== 0) {
|
|
1011
|
+
throw new InternalNetworkConnectivityError(`Internal network connectivity check failed: MITM round-trip exited ${mitmResult.exitCode}; ` +
|
|
1012
|
+
`the sidecar could not reach the host proxy.`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Verifies that Docker's writable layer has space before an agent starts.
|
|
1017
|
+
* Docker Desktop can keep creating/starting containers after its VM disk is
|
|
1018
|
+
* full, while writes inside the container fail with ENOSPC. Claude Code then
|
|
1019
|
+
* exits 0 with no output after its initialization timeout, which otherwise
|
|
1020
|
+
* looks like a networking failure.
|
|
1021
|
+
*/
|
|
1022
|
+
export async function checkDockerContainerWritableStorage(docker, containerId) {
|
|
1023
|
+
const result = await docker.exec(containerId, ['/bin/sh', '-c', 'probe="${HOME:-/home/codespace}/.ironcurtain-write-probe-$$"; mkdir "$probe" && rmdir "$probe"'], 5_000);
|
|
1024
|
+
if (result.exitCode === 0)
|
|
1025
|
+
return;
|
|
1026
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`;
|
|
1027
|
+
const guidance = /ENOSPC|no space left on device/i.test(detail)
|
|
1028
|
+
? 'Docker storage may be full; inspect it with `docker system df` and reclaim unused data or increase the disk limit.'
|
|
1029
|
+
: 'Inspect the container state, Docker daemon logs, and filesystem permissions.';
|
|
1030
|
+
throw new Error(`Docker container writable storage check failed: ${detail}. ${guidance}`);
|
|
879
1031
|
}
|
|
880
1032
|
/**
|
|
881
1033
|
* Writes /etc/apt/apt.conf.d/90-ironcurtain-proxy inside a running
|
|
882
|
-
* container via exec (as root). Used by
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
1034
|
+
* container via exec (as root). Used by apple-container in both batch
|
|
1035
|
+
* and PTY modes — the single-file bind mount the Docker topologies use
|
|
1036
|
+
* is unworkable there: on `tcp-hostonly` (pre-1.1.0) `--mount` rejected
|
|
1037
|
+
* non-directory sources, and on `uds` (1.1.0+) a `-v <dir>/file` mount
|
|
1038
|
+
* whose source nests under the already-shared orientation dir silently
|
|
1039
|
+
* drops that dir share. The URL is built from IronCurtain's own
|
|
1040
|
+
* gateway/loopback address and OS-assigned port — runtime-generated
|
|
1041
|
+
* values, not untrusted input — so embedding it in the sh script is
|
|
1042
|
+
* safe.
|
|
888
1043
|
*/
|
|
889
|
-
export async function
|
|
1044
|
+
export async function writeAptProxyConfigViaExec(docker, containerId, proxyUrl) {
|
|
890
1045
|
const aptWrite = await docker.exec(containerId, [
|
|
891
1046
|
'sh',
|
|
892
1047
|
'-c',
|
|
@@ -963,6 +1118,15 @@ export function resolveRealKey(host, config, oauthAccessToken) {
|
|
|
963
1118
|
case 'generativelanguage.googleapis.com':
|
|
964
1119
|
key = config.userConfig.googleApiKey;
|
|
965
1120
|
break;
|
|
1121
|
+
case OPENROUTER_HOST: {
|
|
1122
|
+
// OpenRouter uses a static bearer key from the stamped active profile
|
|
1123
|
+
// (§7.5). The same host serves all three agents, so this single case
|
|
1124
|
+
// covers them. `isManagedOAuthHost` never matches openrouter.ai, so no
|
|
1125
|
+
// OAuth token is involved here.
|
|
1126
|
+
const profile = config.activeProviderProfile;
|
|
1127
|
+
key = profile?.type === 'openrouter' ? profile.apiKey : '';
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
966
1130
|
default:
|
|
967
1131
|
logger.warn(`No API key mapping for unknown provider host: ${host}`);
|
|
968
1132
|
return '';
|
|
@@ -1077,6 +1241,42 @@ async function buildImageFromCleanContext(docker, image, dockerDir, dockerfile,
|
|
|
1077
1241
|
rmSync(tmpContext, { recursive: true, force: true });
|
|
1078
1242
|
}
|
|
1079
1243
|
}
|
|
1244
|
+
/**
|
|
1245
|
+
* `${image}@${buildHash}` keys whose baked CLI version has already been logged
|
|
1246
|
+
* this process, so the diagnostic probe (a throwaway `docker run --version`)
|
|
1247
|
+
* runs at most once per built image rather than on every session/workflow-state.
|
|
1248
|
+
* Keyed by build hash — not the tag alone — so a same-process rebuild (which
|
|
1249
|
+
* keeps the `:latest` tag but changes `ironcurtain.build-hash`) re-probes and
|
|
1250
|
+
* re-logs the possibly-changed version, which is the whole point of surfacing
|
|
1251
|
+
* drift.
|
|
1252
|
+
*/
|
|
1253
|
+
const loggedAgentVersions = new Set();
|
|
1254
|
+
/**
|
|
1255
|
+
* Best-effort: log the agent CLI version baked into `image`. The agent CLI is
|
|
1256
|
+
* installed UNPINNED in the Docker image, so any change to a hashed build input
|
|
1257
|
+
* (Dockerfile / *.sh) rebuilds the image and can silently pull a newer agent —
|
|
1258
|
+
* see issue #367, where a Claude Code minor bump flipped subagents to run in the
|
|
1259
|
+
* background, breaking the one-shot `claude -p` workflow model. Surfacing the
|
|
1260
|
+
* resolved version makes that drift visible. Skipped when the adapter defines no
|
|
1261
|
+
* probe or the runtime can't run an ephemeral container; never throws.
|
|
1262
|
+
*/
|
|
1263
|
+
async function logResolvedAgentVersion(runtime, image, buildHash, versionProbe) {
|
|
1264
|
+
if (!versionProbe || versionProbe.length === 0 || !runtime.probeImageVersion)
|
|
1265
|
+
return;
|
|
1266
|
+
const cacheKey = `${image}@${buildHash}`;
|
|
1267
|
+
if (loggedAgentVersions.has(cacheKey))
|
|
1268
|
+
return;
|
|
1269
|
+
loggedAgentVersions.add(cacheKey);
|
|
1270
|
+
try {
|
|
1271
|
+
const version = await runtime.probeImageVersion(image, versionProbe);
|
|
1272
|
+
if (version) {
|
|
1273
|
+
logger.info(`[docker] ${image} agent version (unpinned): ${version}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
catch {
|
|
1277
|
+
// Diagnostic only — never disturb infra prep.
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1080
1280
|
// `docker` is typed `ContainerRuntime` (the apple-container generalization of
|
|
1081
1281
|
// the former `DockerManager`); exported because callers/tests import it.
|
|
1082
1282
|
export async function ensureImage(image, docker, ca) {
|