diff --git a/docs/environment.md b/docs/environment.md index 946f42af..2b3e6115 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -25,6 +25,12 @@ When using `sudo -E`, these host variables are automatically passed: `GITHUB_TOK The following are always set/overridden: `PATH` (container values). +### Self-hosted runner home directory support + +AWF derives the effective home directory at runtime from the host environment (`$HOME`, with sudo-aware handling), not from a hardcoded `/home/runner` path. + +This means self-hosted Linux runners with non-standard service-account homes are supported, as long as `$HOME` is set correctly before invoking `awf`. + Variables from `--env` flags override everything else. **Proxy variables set automatically:** `HTTP_PROXY`, `HTTPS_PROXY`, and `https_proxy` are always set to point to the Squid proxy (`http://172.30.0.10:3128`). Note that lowercase `http_proxy` is intentionally **not** set — some curl builds on Ubuntu 22.04 ignore uppercase `HTTP_PROXY` for HTTP URLs (httpoxy mitigation), so HTTP traffic falls through to iptables DNAT interception instead. iptables DNAT serves as a defense-in-depth fallback for both HTTP and HTTPS. diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 659d2318..eb52ca07 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -878,6 +878,37 @@ describe('docker-manager', () => { expect(volumes).toContain(`/tmp/awf-test/agent-logs:/host${homeDir}/.copilot/logs:rw`); }); + it('should create missing .copilot directory and mount it when using non-standard HOME path', () => { + const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-home-')); + const originalHome = process.env.HOME; + const originalSudoUser = process.env.SUDO_USER; + delete process.env.SUDO_USER; + process.env.HOME = fakeHome; + + try { + const copilotDir = path.join(fakeHome, '.copilot'); + expect(fs.existsSync(copilotDir)).toBe(false); + + const result = generateDockerCompose(mockConfig, mockNetworkConfig); + const volumes = result.services.agent.volumes as string[]; + + expect(fs.existsSync(copilotDir)).toBe(true); + expect(volumes).toContain(`${fakeHome}/.copilot:/host${fakeHome}/.copilot:rw`); + } finally { + if (originalHome !== undefined) { + process.env.HOME = originalHome; + } else { + delete process.env.HOME; + } + if (originalSudoUser !== undefined) { + process.env.SUDO_USER = originalSudoUser; + } else { + delete process.env.SUDO_USER; + } + fs.rmSync(fakeHome, { recursive: true, force: true }); + } + }); + it('should use sessionStateDir when specified for chroot mounts', () => { const configWithSessionDir = { ...mockConfig, sessionStateDir: '/custom/session-state' }; const result = generateDockerCompose(configWithSessionDir, mockNetworkConfig); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index b127c59e..e903aad0 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -1164,7 +1164,17 @@ export function generateDockerCompose( // Mount ~/.copilot for Copilot CLI (package extraction, MCP config, etc.) // This is safe as ~/.copilot contains only Copilot CLI state, not credentials. // Auth tokens are in COPILOT_GITHUB_TOKEN env var (handled by API proxy sidecar). - agentVolumes.push(`${effectiveHome}/.copilot:/host${effectiveHome}/.copilot:rw`); + const copilotHomeDir = path.join(effectiveHome, '.copilot'); + if (fs.existsSync(copilotHomeDir)) { + try { + fs.accessSync(copilotHomeDir, fs.constants.R_OK | fs.constants.W_OK); + agentVolumes.push(`${copilotHomeDir}:/host${effectiveHome}/.copilot:rw`); + } catch (error) { + logger.warn(`Cannot access ~/.copilot directory at ${copilotHomeDir}; skipping host bind mount. Copilot CLI package extraction and persisted host MCP config may be unavailable. Error: ${error instanceof Error ? error.message : String(error)}`); + } + } else { + logger.debug(`~/.copilot directory does not exist at ${copilotHomeDir}; skipping optional host bind mount.`); + } // Overlay session-state and logs from AWF workDir so events.jsonl and logs are // captured in the workDir instead of written to the host's ~/.copilot.