diff --git a/containers/agent/one-shot-token/one-shot-token.c b/containers/agent/one-shot-token/one-shot-token.c index 3b8cda82b..d3ee20cea 100644 --- a/containers/agent/one-shot-token/one-shot-token.c +++ b/containers/agent/one-shot-token/one-shot-token.c @@ -290,6 +290,8 @@ char *getenv(const char *name) { } else { /* Already accessed - return cached value */ result = token_cache[token_idx]; + fprintf(stderr, "[one-shot-token] Token %s accessed (cached value: %s)\n", + name, format_token_value(result)); } pthread_mutex_unlock(&token_mutex); @@ -318,16 +320,22 @@ char *secure_getenv(const char *name) { return getenv(name); } + /* Initialize token list on first call (thread-safe) */ + pthread_mutex_lock(&token_mutex); + if (!tokens_initialized) { + init_token_list(); + } + + /* Get token index while holding mutex to avoid race with initialization */ int token_idx = get_token_index(name); - /* Not a sensitive token - pass through to real secure_getenv */ + /* Not a sensitive token - release mutex and pass through to real secure_getenv */ if (token_idx < 0) { + pthread_mutex_unlock(&token_mutex); return real_secure_getenv(name); } - /* Sensitive token - handle cached access with secure_getenv semantics */ - pthread_mutex_lock(&token_mutex); - + /* Sensitive token - handle cached access with secure_getenv semantics (mutex already held) */ char *result = NULL; if (!token_accessed[token_idx]) { @@ -354,6 +362,8 @@ char *secure_getenv(const char *name) { } else { /* Already accessed - return cached value */ result = token_cache[token_idx]; + fprintf(stderr, "[one-shot-token] Token %s accessed (cached value: %s) (via secure_getenv)\n", + name, format_token_value(result)); } pthread_mutex_unlock(&token_mutex); diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 73a2a2d32..636274feb 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -489,6 +489,26 @@ export function generateDockerCompose( // - One-shot token LD_PRELOAD library: /host/tmp/awf-lib/one-shot-token.so agentVolumes.push('/tmp:/host/tmp:rw'); + // Mount ~/.copilot for GitHub Copilot CLI (package extraction, config, logs) + // This is safe as ~/.copilot contains only Copilot CLI state, not credentials + agentVolumes.push(`${effectiveHome}/.copilot:/host${effectiveHome}/.copilot:rw`); + + // Mount ~/.cache, ~/.config, ~/.local for CLI tool state management (Claude Code, etc.) + // These directories are safe to mount as they contain application state, not credentials + // Note: Specific credential files within ~/.config (like ~/.config/gh/hosts.yml) are + // still blocked via /dev/null overlays applied later in the code + agentVolumes.push(`${effectiveHome}/.cache:/host${effectiveHome}/.cache:rw`); + agentVolumes.push(`${effectiveHome}/.config:/host${effectiveHome}/.config:rw`); + agentVolumes.push(`${effectiveHome}/.local:/host${effectiveHome}/.local:rw`); + + // Mount ~/.anthropic for Claude Code state and configuration + // This is safe as ~/.anthropic contains only Claude-specific state, not credentials + agentVolumes.push(`${effectiveHome}/.anthropic:/host${effectiveHome}/.anthropic:rw`); + + // Mount ~/.claude for Claude CLI state and configuration + // This is safe as ~/.claude contains only Claude-specific state, not credentials + agentVolumes.push(`${effectiveHome}/.claude:/host${effectiveHome}/.claude:rw`); + // Minimal /etc - only what's needed for runtime // Note: /etc/shadow is NOT mounted (contains password hashes) agentVolumes.push( diff --git a/tests/integration/chroot-copilot-home.test.ts b/tests/integration/chroot-copilot-home.test.ts new file mode 100644 index 000000000..ea49a075d --- /dev/null +++ b/tests/integration/chroot-copilot-home.test.ts @@ -0,0 +1,87 @@ +/** + * Chroot Copilot Home Directory Tests + * + * These tests verify that the GitHub Copilot CLI can access and write + * to ~/.copilot directory in chroot mode. This is essential for: + * - Package extraction (CLI extracts bundled packages to ~/.copilot/pkg) + * - Configuration storage + * - Log file management + * + * The fix mounts ~/.copilot at /host~/.copilot in chroot mode to enable + * write access while maintaining security (no full HOME mount). + */ + +/// + +import { describe, test, expect, beforeAll, afterAll } from '@jest/globals'; +import { createRunner, AwfRunner } from '../fixtures/awf-runner'; +import { cleanup } from '../fixtures/cleanup'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +describe('Chroot Copilot Home Directory Access', () => { + let runner: AwfRunner; + let testCopilotDir: string; + + beforeAll(async () => { + await cleanup(false); + runner = createRunner(); + + // Ensure ~/.copilot exists on the host (as the workflow does) + testCopilotDir = path.join(os.homedir(), '.copilot'); + if (!fs.existsSync(testCopilotDir)) { + fs.mkdirSync(testCopilotDir, { recursive: true, mode: 0o755 }); + } + }); + + afterAll(async () => { + await cleanup(false); + }); + + test('should be able to write to ~/.copilot directory', async () => { + const result = await runner.runWithSudo( + 'mkdir -p ~/.copilot/test && echo "test-content" > ~/.copilot/test/file.txt && cat ~/.copilot/test/file.txt', + { + allowDomains: ['localhost'], + logLevel: 'debug', + timeout: 60000, + enableChroot: true, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('test-content'); + }, 120000); + + test('should be able to create nested directories in ~/.copilot', async () => { + // Simulate what Copilot CLI does: create pkg/linux-x64/VERSION + const result = await runner.runWithSudo( + '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', + { + allowDomains: ['localhost'], + logLevel: 'debug', + timeout: 60000, + enableChroot: true, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('package-extracted'); + }, 120000); + + test('should verify ~/.copilot is writable with correct permissions', async () => { + const result = await runner.runWithSudo( + 'touch ~/.copilot/write-test && rm ~/.copilot/write-test && echo "write-success"', + { + allowDomains: ['localhost'], + logLevel: 'debug', + timeout: 60000, + enableChroot: true, + } + ); + + expect(result).toSucceed(); + expect(result.stdout).toContain('write-success'); + }, 120000); +});