From d2f16be7552aa9cf856b186108d9f5de19bb8b95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 03:46:09 +0000 Subject: [PATCH 1/2] Initial plan From d175ce4892f654ddcb12e61e2f4ec457384891b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 03:59:15 +0000 Subject: [PATCH 2/2] fix: ARC/DinD sibling socket auto-detection for path prefix - Detect non-standard unix sockets and AWF_DIND=1 in resolveDockerHostPathPrefix - Add dindHint flag to warn operators about potential split filesystem - Add /tmp/gh-aw to DinD probe candidate prefixes for ARC setups - Update tests for new behavior --- src/commands/validate-options.test.ts | 1 + .../validators/config-assembly.test.ts | 2 +- src/commands/validators/network-options.ts | 14 ++++ src/dind-probe.test.ts | 28 +++++++ src/dind-probe.ts | 2 +- src/option-parsers-misc.test.ts | 78 ++++++++++++++++++- src/option-parsers.ts | 47 +++++++++-- 7 files changed, 162 insertions(+), 10 deletions(-) diff --git a/src/commands/validate-options.test.ts b/src/commands/validate-options.test.ts index bdebd765d..531f24187 100644 --- a/src/commands/validate-options.test.ts +++ b/src/commands/validate-options.test.ts @@ -116,6 +116,7 @@ describe('validateOptions', () => { mockedOptionParsers.resolveDockerHostPathPrefix.mockReturnValue({ dockerHostPathPrefix: undefined, autoApplied: false, + dindHint: false, }); mockedOptionParsers.parseEnvironmentVariables.mockReturnValue({ success: true, env: {} }); mockedOptionParsers.parseVolumeMounts.mockReturnValue({ success: true, mounts: [] }); diff --git a/src/commands/validators/config-assembly.test.ts b/src/commands/validators/config-assembly.test.ts index a2a2a2721..2e0c20c2d 100644 --- a/src/commands/validators/config-assembly.test.ts +++ b/src/commands/validators/config-assembly.test.ts @@ -105,7 +105,7 @@ describe('config-assembly', () => { dnsOverHttps: undefined, resolvedCopilotApiTarget: undefined, resolvedCopilotApiBasePath: undefined, - dockerHostPathPrefixResolution: { dockerHostPathPrefix: undefined, autoApplied: false }, + dockerHostPathPrefixResolution: { dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }, }); const createMinimalAgentOptions = (): AgentOptionsResult => ({ diff --git a/src/commands/validators/network-options.ts b/src/commands/validators/network-options.ts index cc38252bd..ef87d6d0a 100644 --- a/src/commands/validators/network-options.ts +++ b/src/commands/validators/network-options.ts @@ -61,6 +61,20 @@ export function validateNetworkOptions(options: Record): Networ '⚠️ If your Docker daemon uses a split runner/daemon filesystem, set --docker-host-path-prefix (for example: /host).', ); } + if (dockerHostPathPrefixResolution.dindHint && !dockerHostPathPrefixResolution.dockerHostPathPrefix) { + logger.warn( + '⚠️ Non-standard DOCKER_HOST unix socket or AWF_DIND=1 detected — this typically indicates an ARC/DinD', + ); + logger.warn( + ' setup where the runner and Docker daemon have separate root filesystems.', + ); + logger.warn( + ' If bind mounts fail, set --docker-host-path-prefix to the path prefix where the runner filesystem', + ); + logger.warn( + ' is visible inside the daemon (e.g. --docker-host-path-prefix /tmp/gh-aw).', + ); + } // --- Domain resolution -------------------------------------------------- diff --git a/src/dind-probe.test.ts b/src/dind-probe.test.ts index ea6233325..f597bd857 100644 --- a/src/dind-probe.test.ts +++ b/src/dind-probe.test.ts @@ -84,6 +84,29 @@ describe('probeSplitFilesystem', () => { expect(mockedExeca).toHaveBeenCalledTimes(4); }); + it('returns /tmp/gh-aw when /host and /runner fail but /tmp/gh-aw prefix works', async () => { + mockDockerReachable(); + // Direct mount: file not found + mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); + // /host prefix: file not found + mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); + // /runner prefix: file not found + mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); + // /tmp/gh-aw prefix succeeds + mockedExeca.mockResolvedValueOnce({ exitCode: 0 } as any); + + const result = await probeSplitFilesystem(probeDir); + + expect(result.prefix).toBe('/tmp/gh-aw'); + expect(result.splitDetected).toBe(true); + expect(result.inconclusive).toBe(false); + // 1 docker info + 4 probes + expect(mockedExeca).toHaveBeenCalledTimes(5); + // Verify /tmp/gh-aw call uses prefixed path + const ghAwCallArgs = mockedExeca.mock.calls[4][1] as string[]; + expect(ghAwCallArgs).toContain(`/tmp/gh-aw${probeDir}:/probe:ro`); + }); + it('returns splitDetected=true when no candidate prefix works', async () => { mockDockerReachable(); // Direct mount: file not found @@ -92,12 +115,16 @@ describe('probeSplitFilesystem', () => { mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); // /runner prefix: file not found mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); + // /tmp/gh-aw prefix: file not found + mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); const result = await probeSplitFilesystem(probeDir); expect(result.prefix).toBeUndefined(); expect(result.splitDetected).toBe(true); expect(result.inconclusive).toBe(false); + // 1 docker info + 4 probes + expect(mockedExeca).toHaveBeenCalledTimes(5); }); it('returns inconclusive when Docker daemon is unreachable (fail-fast)', async () => { @@ -181,6 +208,7 @@ describe('probeSplitFilesystem', () => { mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); + mockedExeca.mockResolvedValueOnce({ exitCode: 1 } as any); await probeSplitFilesystem(probeDir); diff --git a/src/dind-probe.ts b/src/dind-probe.ts index 7705a1404..625b1db57 100644 --- a/src/dind-probe.ts +++ b/src/dind-probe.ts @@ -23,7 +23,7 @@ import { getLocalDockerEnv } from './docker-host'; import { logger } from './logger'; /** Candidate prefixes to try when split filesystem is detected */ -const CANDIDATE_PREFIXES = ['/host', '/runner']; +const CANDIDATE_PREFIXES = ['/host', '/runner', '/tmp/gh-aw']; /** Timeout for each docker run probe (ms) */ const PROBE_TIMEOUT_MS = 10000; diff --git a/src/option-parsers-misc.test.ts b/src/option-parsers-misc.test.ts index a48151a09..3a4fe9a55 100644 --- a/src/option-parsers-misc.test.ts +++ b/src/option-parsers-misc.test.ts @@ -332,17 +332,89 @@ describe('checkDockerHost', () => { describe('resolveDockerHostPathPrefix', () => { it('returns explicit prefix when provided', () => { const result = resolveDockerHostPathPrefix({ valid: false, error: 'external DOCKER_HOST' }, '/daemon-root'); - expect(result).toEqual({ dockerHostPathPrefix: '/daemon-root', autoApplied: false }); + expect(result).toEqual({ dockerHostPathPrefix: '/daemon-root', autoApplied: false, dindHint: false }); }); it('does not auto-apply a prefix for external DOCKER_HOST when none is provided', () => { const result = resolveDockerHostPathPrefix({ valid: false, error: 'external DOCKER_HOST' }, undefined); - expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false }); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }); }); it('returns undefined when DOCKER_HOST is local and no prefix is provided', () => { const result = resolveDockerHostPathPrefix({ valid: true }, undefined); - expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false }); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }); + }); + + it('sets dindHint when DOCKER_HOST is a non-standard unix socket', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + undefined, + { DOCKER_HOST: 'unix:///tmp/docker-sibling.sock' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: true }); + }); + + it('does not set dindHint for the default /var/run/docker.sock socket', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + undefined, + { DOCKER_HOST: 'unix:///var/run/docker.sock' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }); + }); + + it('does not set dindHint for the /run/docker.sock socket', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + undefined, + { DOCKER_HOST: 'unix:///run/docker.sock' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }); + }); + + it('sets dindHint when AWF_DIND=1 is set', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + undefined, + { AWF_DIND: '1' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: true }); + }); + + it('does not set dindHint when AWF_DIND is not 1', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + undefined, + { AWF_DIND: '0' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }); + }); + + it('does not set dindHint for TCP DOCKER_HOST (checked by checkDockerHost separately)', () => { + const result = resolveDockerHostPathPrefix( + { valid: false, error: 'external' }, + undefined, + { DOCKER_HOST: 'tcp://localhost:2375' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: undefined, autoApplied: false, dindHint: false }); + }); + + it('explicit prefix wins and suppresses dindHint even when non-standard socket is set', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + '/tmp/gh-aw', + { DOCKER_HOST: 'unix:///tmp/docker-sibling.sock' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: '/tmp/gh-aw', autoApplied: false, dindHint: false }); + }); + + it('explicit prefix wins and suppresses dindHint when AWF_DIND=1', () => { + const result = resolveDockerHostPathPrefix( + { valid: true }, + '/host', + { AWF_DIND: '1' }, + ); + expect(result).toEqual({ dockerHostPathPrefix: '/host', autoApplied: false, dindHint: false }); }); }); diff --git a/src/option-parsers.ts b/src/option-parsers.ts index 1a472703f..5b55c04aa 100644 --- a/src/option-parsers.ts +++ b/src/option-parsers.ts @@ -139,22 +139,59 @@ export function checkDockerHost( }; } +/** + * Standard Docker socket paths that indicate a local daemon on the same + * filesystem as the runner. Any other unix:// path is treated as a potential + * sibling-daemon (ARC/DinD) socket that may use a split filesystem. + */ +const DEFAULT_DOCKER_SOCKET_URIS = [ + 'unix:///var/run/docker.sock', + 'unix:///run/docker.sock', +]; + +/** + * Returns `true` when `DOCKER_HOST` is a unix socket on a non-default path, + * which typically indicates a sibling daemon pod in ARC/DinD deployments. + * These setups bind-mount the daemon's socket into the runner pod, meaning + * the runner and daemon may have separate root filesystems. + */ +function isSiblingDaemonSocket(env: Record): boolean { + const dockerHost = env['DOCKER_HOST']; + if (!dockerHost || !dockerHost.startsWith('unix://')) return false; + return !DEFAULT_DOCKER_SOCKET_URIS.includes(dockerHost); +} + /** * Resolves the effective Docker host path prefix for bind mount translation. * - * If an explicit prefix is provided, it wins. Otherwise, no prefix is applied. + * If an explicit prefix is provided, it wins. Otherwise the function inspects + * the environment for DinD indicators: + * - `DOCKER_HOST` pointing at a non-standard unix socket (sibling daemon pod) + * - `AWF_DIND=1` set explicitly by the operator + * + * When a DinD indicator is found, `dindHint` is set to `true` so callers can + * emit actionable warnings. The actual prefix is NOT auto-applied here — the + * `probeSplitFilesystem` probe in `main-action.ts` discovers it at runtime. + * + * @param _dockerHostCheck - Result of {@link checkDockerHost} (unused; kept for + * interface symmetry with the caller in {@link validateNetworkOptions}). + * @param explicitPrefix - Value from the `--docker-host-path-prefix` flag. + * @param env - Environment variables to inspect (defaults to `process.env`). */ export function resolveDockerHostPathPrefix( _dockerHostCheck: { valid: true } | { valid: false; error: string }, - explicitPrefix: string | undefined -): { dockerHostPathPrefix?: string; autoApplied: boolean } { + explicitPrefix: string | undefined, + env: Record = process.env +): { dockerHostPathPrefix?: string; autoApplied: boolean; dindHint: boolean } { const trimmedExplicitPrefix = explicitPrefix?.trim(); if (trimmedExplicitPrefix) { - return { dockerHostPathPrefix: trimmedExplicitPrefix, autoApplied: false }; + return { dockerHostPathPrefix: trimmedExplicitPrefix, autoApplied: false, dindHint: false }; } - return { dockerHostPathPrefix: undefined, autoApplied: false }; + const dindHint = env['AWF_DIND'] === '1' || isSiblingDaemonSocket(env); + + return { dockerHostPathPrefix: undefined, autoApplied: false, dindHint }; } /**