Skip to content

Commit d414258

Browse files
authored
fix: make node available and validated for copilot chroot startup
Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/345ed70b-1477-4fae-bb25-801df1d3fab2
1 parent 1444acc commit d414258

3 files changed

Lines changed: 48 additions & 3 deletions

File tree

containers/agent/entrypoint.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,16 @@ AWFEOF
706706
echo 'fi' >> "/host${SCRIPT_FILE}"
707707
echo 'mkdir -p "$NPM_CONFIG_PREFIX/bin" 2>/dev/null' >> "/host${SCRIPT_FILE}"
708708
echo 'export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"' >> "/host${SCRIPT_FILE}"
709+
if [ "${AWF_REQUIRE_NODE:-}" = "1" ]; then
710+
cat >> "/host${SCRIPT_FILE}" << 'AWFEOF'
711+
if ! command -v node >/dev/null 2>&1; then
712+
echo "[entrypoint][ERROR] Copilot CLI requires Node.js, but 'node' is not available inside AWF chroot."
713+
echo "[entrypoint][ERROR] Ensure Node.js is installed on the runner and reachable from PATH inside the chroot."
714+
echo "[entrypoint][ERROR] Common locations: /opt/hostedtoolcache/... or \$HOME/.nvm/..."
715+
exit 127
716+
fi
717+
AWFEOF
718+
fi
709719
# Append the actual command arguments
710720
# Docker CMD passes commands as ['/bin/bash', '-c', 'command_string'].
711721
# Instead of writing the full [bash, -c, cmd] via printf '%q' (which creates

src/docker-manager.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ describe('docker-manager', () => {
857857
expect(volumes).toContain(`${workspaceDir}:/host${workspaceDir}:rw`);
858858
});
859859

860-
it('should mount Rust toolchain, npm cache, and CLI state directories', () => {
860+
it('should mount Rust toolchain, Node/npm caches, and CLI state directories', () => {
861861
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
862862
const agent = result.services.agent;
863863
const volumes = agent.volumes as string[];
@@ -868,6 +868,8 @@ describe('docker-manager', () => {
868868
expect(volumes).toContain(`${homeDir}/.rustup:/host${homeDir}/.rustup:rw`);
869869
// npm cache
870870
expect(volumes).toContain(`${homeDir}/.npm:/host${homeDir}/.npm:rw`);
871+
// nvm-managed Node.js cache/installations
872+
expect(volumes).toContain(`${homeDir}/.nvm:/host${homeDir}/.nvm:rw`);
871873
// CLI state directories
872874
expect(volumes).toContain(`${homeDir}/.claude:/host${homeDir}/.claude:rw`);
873875
expect(volumes).toContain(`${homeDir}/.anthropic:/host${homeDir}/.anthropic:rw`);
@@ -944,6 +946,26 @@ describe('docker-manager', () => {
944946
expect(environment.AWF_CHROOT_ENABLED).toBe('true');
945947
});
946948

949+
it('should set AWF_REQUIRE_NODE when running Copilot CLI command', () => {
950+
const result = generateDockerCompose(
951+
{ ...mockConfig, agentCommand: 'copilot --version' },
952+
mockNetworkConfig,
953+
);
954+
const environment = result.services.agent.environment as Record<string, string>;
955+
956+
expect(environment.AWF_REQUIRE_NODE).toBe('1');
957+
});
958+
959+
it('should not set AWF_REQUIRE_NODE for non-Copilot commands', () => {
960+
const result = generateDockerCompose(
961+
{ ...mockConfig, agentCommand: 'echo test' },
962+
mockNetworkConfig,
963+
);
964+
const environment = result.services.agent.environment as Record<string, string>;
965+
966+
expect(environment.AWF_REQUIRE_NODE).toBeUndefined();
967+
});
968+
947969
it('should pass GOROOT, CARGO_HOME, RUSTUP_HOME, JAVA_HOME, DOTNET_ROOT, BUN_INSTALL to container when env vars are set', () => {
948970
const originalGoroot = process.env.GOROOT;
949971
const originalCargoHome = process.env.CARGO_HOME;
@@ -3641,7 +3663,7 @@ describe('docker-manager', () => {
36413663
// Verify chroot home subdirectories were created
36423664
const expectedDirs = [
36433665
'.copilot', '.cache', '.config', '.local',
3644-
'.anthropic', '.claude', '.gemini', '.cargo', '.rustup', '.npm',
3666+
'.anthropic', '.claude', '.gemini', '.cargo', '.rustup', '.npm', '.nvm',
36453667
];
36463668
for (const dir of expectedDirs) {
36473669
expect(fs.existsSync(path.join(fakeHome, dir))).toBe(true);

src/docker-manager.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,16 @@ export function generateDockerCompose(
815815
AWF_ONE_SHOT_TOKENS: 'COPILOT_GITHUB_TOKEN,GITHUB_TOKEN,GH_TOKEN,GITHUB_API_TOKEN,GITHUB_PAT,GH_ACCESS_TOKEN,OPENAI_API_KEY,OPENAI_KEY,ANTHROPIC_API_KEY,CLAUDE_API_KEY,CODEX_API_KEY,COPILOT_API_KEY,COPILOT_PROVIDER_API_KEY',
816816
};
817817

818+
// Copilot CLI requires Node.js. Ask the agent entrypoint to fail fast with a
819+
// clear diagnostic if node is not reachable inside the chroot before startup.
820+
if (
821+
config.copilotGithubToken
822+
|| config.copilotApiKey
823+
|| /\bcopilot\b/.test(config.agentCommand)
824+
) {
825+
environment.AWF_REQUIRE_NODE = '1';
826+
}
827+
818828
// When api-proxy is enabled with Copilot, set placeholder tokens early
819829
// so --env-all won't override them with real values from host environment
820830
if (config.enableApiProxy && config.copilotGithubToken) {
@@ -1222,6 +1232,9 @@ export function generateDockerCompose(
12221232
// npm requires write access to ~/.npm for caching packages and writing logs
12231233
agentVolumes.push(`${effectiveHome}/.npm:/host${effectiveHome}/.npm:rw`);
12241234

1235+
// Mount ~/.nvm for Node.js installations managed by nvm on self-hosted runners
1236+
agentVolumes.push(`${effectiveHome}/.nvm:/host${effectiveHome}/.nvm:rw`);
1237+
12251238
// Minimal /etc - only what's needed for runtime
12261239
// Note: /etc/shadow is NOT mounted (contains password hashes)
12271240
agentVolumes.push(
@@ -2202,7 +2215,7 @@ export async function writeConfigs(config: WrapperConfig): Promise<void> {
22022215
// Ensure source directories for subdirectory mounts exist with correct ownership
22032216
const chrootHomeDirs = [
22042217
'.copilot', '.cache', '.config', '.local',
2205-
'.anthropic', '.claude', '.gemini', '.cargo', '.rustup', '.npm',
2218+
'.anthropic', '.claude', '.gemini', '.cargo', '.rustup', '.npm', '.nvm',
22062219
];
22072220
for (const dir of chrootHomeDirs) {
22082221
const dirPath = path.join(effectiveHome, dir);

0 commit comments

Comments
 (0)