Skip to content

Commit 2150520

Browse files
Copilotlpcox
andcommitted
fix(docker): mount ~/.copilot in chroot mode for copilot cli
GitHub Copilot CLI needs to extract bundled packages to ~/.copilot/pkg directory. In chroot mode, this directory was not accessible, causing "EACCES: permission denied" errors when trying to create directories. The fix mounts ~/.copilot at /host~/.copilot in chroot mode, allowing the CLI to create necessary directories while maintaining security (no full HOME mount). Added tests to verify ~/.copilot is writable in chroot mode. Fixes: https://github.com/github/gh-aw-firewall/actions/runs/21912194865/job/63270212999 Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 9359196 commit 2150520

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

src/docker-manager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,10 @@ export function generateDockerCompose(
489489
// - One-shot token LD_PRELOAD library: /host/tmp/awf-lib/one-shot-token.so
490490
agentVolumes.push('/tmp:/host/tmp:rw');
491491

492+
// Mount ~/.copilot for GitHub Copilot CLI (package extraction, config, logs)
493+
// This is safe as ~/.copilot contains only Copilot CLI state, not credentials
494+
agentVolumes.push(`${effectiveHome}/.copilot:/host${effectiveHome}/.copilot:rw`);
495+
492496
// Minimal /etc - only what's needed for runtime
493497
// Note: /etc/shadow is NOT mounted (contains password hashes)
494498
agentVolumes.push(
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Chroot Copilot Home Directory Tests
3+
*
4+
* These tests verify that the GitHub Copilot CLI can access and write
5+
* to ~/.copilot directory in chroot mode. This is essential for:
6+
* - Package extraction (CLI extracts bundled packages to ~/.copilot/pkg)
7+
* - Configuration storage
8+
* - Log file management
9+
*
10+
* The fix mounts ~/.copilot at /host~/.copilot in chroot mode to enable
11+
* write access while maintaining security (no full HOME mount).
12+
*/
13+
14+
/// <reference path="../jest-custom-matchers.d.ts" />
15+
16+
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
17+
import { createRunner, AwfRunner } from '../fixtures/awf-runner';
18+
import { cleanup } from '../fixtures/cleanup';
19+
import * as fs from 'fs';
20+
import * as os from 'os';
21+
import * as path from 'path';
22+
23+
describe('Chroot Copilot Home Directory Access', () => {
24+
let runner: AwfRunner;
25+
let testCopilotDir: string;
26+
27+
beforeAll(async () => {
28+
await cleanup(false);
29+
runner = createRunner();
30+
31+
// Ensure ~/.copilot exists on the host (as the workflow does)
32+
testCopilotDir = path.join(os.homedir(), '.copilot');
33+
if (!fs.existsSync(testCopilotDir)) {
34+
fs.mkdirSync(testCopilotDir, { recursive: true, mode: 0o755 });
35+
}
36+
});
37+
38+
afterAll(async () => {
39+
await cleanup(false);
40+
});
41+
42+
test('should be able to write to ~/.copilot directory', async () => {
43+
const result = await runner.runWithSudo(
44+
'mkdir -p ~/.copilot/test && echo "test-content" > ~/.copilot/test/file.txt && cat ~/.copilot/test/file.txt',
45+
{
46+
allowDomains: ['localhost'],
47+
logLevel: 'debug',
48+
timeout: 60000,
49+
enableChroot: true,
50+
}
51+
);
52+
53+
expect(result).toSucceed();
54+
expect(result.stdout).toContain('test-content');
55+
}, 120000);
56+
57+
test('should be able to create nested directories in ~/.copilot', async () => {
58+
// Simulate what Copilot CLI does: create pkg/linux-x64/VERSION
59+
const result = await runner.runWithSudo(
60+
'mkdir -p ~/.copilot/pkg/linux-x64/0.0.405 && echo "package-extracted" > ~/.copilot/pkg/linux-x64/0.0.405/marker.txt && cat ~/.copilot/pkg/linux-x64/0.0.405/marker.txt',
61+
{
62+
allowDomains: ['localhost'],
63+
logLevel: 'debug',
64+
timeout: 60000,
65+
enableChroot: true,
66+
}
67+
);
68+
69+
expect(result).toSucceed();
70+
expect(result.stdout).toContain('package-extracted');
71+
}, 120000);
72+
73+
test('should verify ~/.copilot is writable with correct permissions', async () => {
74+
const result = await runner.runWithSudo(
75+
'touch ~/.copilot/write-test && rm ~/.copilot/write-test && echo "write-success"',
76+
{
77+
allowDomains: ['localhost'],
78+
logLevel: 'debug',
79+
timeout: 60000,
80+
enableChroot: true,
81+
}
82+
);
83+
84+
expect(result).toSucceed();
85+
expect(result.stdout).toContain('write-success');
86+
}, 120000);
87+
});

0 commit comments

Comments
 (0)