Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment on lines +881 to +897
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new test covers the happy path where .copilot can be created, but it doesn’t cover the behavior added in the catch branch (mkdir failure → warn + skip mount). Consider adding a test that mocks fs.mkdirSync (or uses a read-only temp dir) to assert the warning is emitted and the .copilot bind mount is omitted, since that’s the main behavioral change for hardening startup.

Copilot uses AI. Check for mistakes.
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);
Expand Down
12 changes: 11 additions & 1 deletion src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading