diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml index b9eaeabb..75599df1 100644 --- a/.github/workflows/agentics-maintenance.yml +++ b/.github/workflows/agentics-maintenance.yml @@ -13,7 +13,7 @@ # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # -# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.42.0). DO NOT EDIT. +# This file was automatically generated by pkg/workflow/maintenance_workflow.go. DO NOT EDIT. # # To regenerate this workflow, run: # gh aw compile @@ -33,7 +33,7 @@ name: Agentic Maintenance on: schedule: - - cron: "37 0 * * *" # Daily (based on minimum expires: 30 days) + - cron: "37 0 * * *" # Daily (based on minimum expires: 7 days) workflow_dispatch: permissions: {} @@ -47,7 +47,7 @@ jobs: pull-requests: write steps: - name: Setup Scripts - uses: github/gh-aw/actions/setup@v0.42.0 + uses: github/gh-aw/actions/setup@v0.42.2-28-gfba53102d with: destination: /opt/gh-aw/actions diff --git a/.github/workflows/build-test-bun.lock.yml b/.github/workflows/build-test-bun.lock.yml index 5fe3ec61..f26299d4 100644 --- a/.github/workflows/build-test-bun.lock.yml +++ b/.github/workflows/build-test-bun.lock.yml @@ -21,7 +21,7 @@ # # Build Test Bun # -# frontmatter-hash: 2f4301a31bfbc25351b8a0af6b02d4f08de0bc7cfdcbc7a480d4344047bfe341 +# frontmatter-hash: ea8a9e8182ac0e2ffe0190b9c0c1afd02a30bbad1c0a9e80c680b35146832058 name: "Build Test Bun" "on": @@ -96,16 +96,17 @@ jobs: uses: github/gh-aw/actions/setup@a7134347103ecf66b4bd422c3e9ce6466d400c02 # v0.42.0 with: destination: /opt/gh-aw/actions - - name: Checkout .github and .agents folders + - name: Create gh-aw temp directory + run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 with: - sparse-checkout: | - .github - .agents - depth: 1 persist-credentials: false - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh + + - name: Setup Bun + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: 'latest' - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -138,8 +139,31 @@ jobs: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.403 - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.5 + - name: Install awf dependencies + run: npm ci + - name: Build awf + run: npm run build + - name: Install awf binary (local) + run: | + WORKSPACE_PATH="${GITHUB_WORKSPACE:-$(pwd)}" + NODE_BIN="$(command -v node)" + if [ ! -d "$WORKSPACE_PATH" ]; then + echo "Workspace path not found: $WORKSPACE_PATH" + exit 1 + fi + if [ ! -x "$NODE_BIN" ]; then + echo "Node binary not found: $NODE_BIN" + exit 1 + fi + if [ ! -d "/usr/local/bin" ]; then + echo "/usr/local/bin is missing" + exit 1 + fi + sudo tee /usr/local/bin/awf > /dev/null < /dev/null < /dev/null <> $GITHUB_ENV - - name: Create gh-aw temp directory - run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh - name: Configure Git credentials env: REPO_NAME: ${{ github.repository }} @@ -144,8 +141,31 @@ jobs: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - name: Install GitHub Copilot CLI run: /opt/gh-aw/actions/install_copilot_cli.sh 0.0.403 - - name: Install awf binary - run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.13.5 + - name: Install awf dependencies + run: npm ci + - name: Build awf + run: npm run build + - name: Install awf binary (local) + run: | + WORKSPACE_PATH="${GITHUB_WORKSPACE:-$(pwd)}" + NODE_BIN="$(command -v node)" + if [ ! -d "$WORKSPACE_PATH" ]; then + echo "Workspace path not found: $WORKSPACE_PATH" + exit 1 + fi + if [ ! -x "$NODE_BIN" ]; then + echo "Node binary not found: $NODE_BIN" + exit 1 + fi + if [ ! -d "/usr/local/bin" ]; then + echo "/usr/local/bin is missing" + exit 1 + fi + sudo tee /usr/local/bin/awf > /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < squid:${SQUID_INTERCEPT_PORT})" +echo "[entrypoint] Proxy configuration:" +echo "[entrypoint] HTTP: intercept mode (iptables DNAT 80 -> squid:${SQUID_INTERCEPT_PORT})" +echo "[entrypoint] HTTPS_PROXY=$HTTPS_PROXY" if [ -n "$HTTP_PROXY" ]; then - echo "[entrypoint] HTTP_PROXY=$HTTP_PROXY (user-provided)" -fi -if [ -n "$HTTPS_PROXY" ]; then - echo "[entrypoint] HTTPS_PROXY=$HTTPS_PROXY (user-provided)" + echo "[entrypoint] HTTP_PROXY=$HTTP_PROXY (user-provided override)" fi # Print network information @@ -189,6 +188,18 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then fi fi + # Copy container's /etc/hosts to /host/etc/hosts (preserves Docker extra_hosts like host.docker.internal) + # Docker adds extra_hosts entries (e.g., host.docker.internal) to the container's /etc/hosts, + # but the host's /etc/hosts doesn't have them. We copy the container's version to the overlay + # filesystem at /host/etc/hosts (not a bind mount, so this doesn't modify the real host). + HOSTS_CREATED=false + if cp /etc/hosts /host/etc/hosts 2>/dev/null; then + HOSTS_CREATED=true + echo "[entrypoint] Hosts configuration copied to chroot (includes host.docker.internal)" + else + echo "[entrypoint][WARN] Could not copy hosts configuration to chroot" + fi + # Determine working directory inside the chroot # AWF_WORKDIR is set by docker-manager.ts (containerWorkDir or HOME) # For chroot mode, paths like /home/user stay the same (no /host prefix) @@ -248,6 +259,12 @@ AWFEOF # Java needs LD_LIBRARY_PATH to find libjli.so and other shared libs echo "export LD_LIBRARY_PATH=\"${AWF_JAVA_HOME}/lib:${AWF_JAVA_HOME}/lib/server:\$LD_LIBRARY_PATH\"" >> "/host${SCRIPT_FILE}" fi + # Bun: Add BUN_INSTALL/bin to PATH if provided (for Bun on GitHub Actions) + if [ -n "${AWF_BUN_INSTALL}" ]; then + echo "[entrypoint] Adding BUN_INSTALL/bin to PATH: ${AWF_BUN_INSTALL}/bin" + echo "export PATH=\"${AWF_BUN_INSTALL}/bin:\$PATH\"" >> "/host${SCRIPT_FILE}" + echo "export BUN_INSTALL=\"${AWF_BUN_INSTALL}\"" >> "/host${SCRIPT_FILE}" + fi # Add GOROOT/bin to PATH if provided (required for Go on GitHub Actions with trimmed binaries) # This ensures the correct Go version is found even if AWF_HOST_PATH has wrong ordering if [ -n "${AWF_GOROOT}" ]; then @@ -292,8 +309,8 @@ AWFEOF # Note: We use capsh inside the chroot because it handles the privilege drop # and user switch atomically. The host must have capsh installed. - # Build cleanup command that restores resolv.conf if it was modified or created - # The backup path uses the chroot perspective (no /host prefix) + # Build cleanup command that restores resolv.conf and /etc/hosts if they were modified + # The backup paths use the chroot perspective (no /host prefix) CLEANUP_CMD="rm -f ${SCRIPT_FILE}" if [ "$RESOLV_MODIFIED" = "true" ]; then # Convert backup path from container perspective (/host/etc/...) to chroot perspective (/etc/...) @@ -305,6 +322,7 @@ AWFEOF CLEANUP_CMD="${CLEANUP_CMD}; rm -f /etc/resolv.conf 2>/dev/null || true" echo "[entrypoint] DNS configuration will be removed on exit" fi + # /etc/hosts cleanup not needed — written to container overlay, not host filesystem exec chroot /host /bin/bash -c " cd '${CHROOT_WORKDIR}' 2>/dev/null || cd / diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 2c0b7a66..e6478715 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -479,9 +479,10 @@ describe('docker-manager', () => { const agent = result.services.agent; const env = agent.environment as Record; - // HTTP_PROXY/HTTPS_PROXY are NOT set; intercept mode (iptables DNAT) handles routing + // HTTP_PROXY is NOT set; intercept mode (iptables DNAT) handles HTTP routing + // HTTPS_PROXY IS set; HTTPS requires CONNECT method through forward proxy port 3128 expect(env.HTTP_PROXY).toBeUndefined(); - expect(env.HTTPS_PROXY).toBeUndefined(); + expect(env.HTTPS_PROXY).toBe('http://172.30.0.10:3128'); expect(env.SQUID_PROXY_HOST).toBe('squid-proxy'); expect(env.SQUID_PROXY_PORT).toBe('3128'); expect(env.SQUID_INTERCEPT_PORT).toBe('3129'); @@ -559,7 +560,8 @@ describe('docker-manager', () => { expect(volumes).toContain('/etc/ca-certificates:/host/etc/ca-certificates:ro'); expect(volumes).toContain('/etc/alternatives:/host/etc/alternatives:ro'); expect(volumes).toContain('/etc/ld.so.cache:/host/etc/ld.so.cache:ro'); - expect(volumes).toContain('/etc/hosts:/host/etc/hosts:ro'); + // /etc/hosts is NOT mounted — entrypoint copies container's /etc/hosts at runtime + // to avoid modifying the host's real /etc/hosts file // Should still include essential mounts expect(volumes).toContain('/tmp:/tmp:rw'); @@ -614,12 +616,12 @@ describe('docker-manager', () => { expect(agent.cap_add).not.toContain('SYS_CHROOT'); }); - it('should not mount /etc/hosts under /host when enableChroot is false', () => { + it('should not mount /etc/hosts in any mode (entrypoint handles it at runtime)', () => { const result = generateDockerCompose(mockConfig, mockNetworkConfig); const agent = result.services.agent; const volumes = agent.volumes as string[]; - expect(volumes).not.toContain('/etc/hosts:/host/etc/hosts:ro'); + expect(volumes.some((v: string) => v.includes('/etc/hosts'))).toBe(false); }); it('should set AWF_CHROOT_ENABLED environment variable when enableChroot is true', () => { @@ -634,14 +636,16 @@ describe('docker-manager', () => { expect(environment.AWF_CHROOT_ENABLED).toBe('true'); }); - it('should pass GOROOT, CARGO_HOME, JAVA_HOME to container when enableChroot is true and env vars are set', () => { + it('should pass GOROOT, CARGO_HOME, JAVA_HOME, BUN_INSTALL to container when enableChroot is true and env vars are set', () => { const originalGoroot = process.env.GOROOT; const originalCargoHome = process.env.CARGO_HOME; const originalJavaHome = process.env.JAVA_HOME; + const originalBunInstall = process.env.BUN_INSTALL; process.env.GOROOT = '/usr/local/go'; process.env.CARGO_HOME = '/home/user/.cargo'; process.env.JAVA_HOME = '/usr/lib/jvm/java-17'; + process.env.BUN_INSTALL = '/home/user/.bun'; try { const configWithChroot = { @@ -655,6 +659,7 @@ describe('docker-manager', () => { expect(environment.AWF_GOROOT).toBe('/usr/local/go'); expect(environment.AWF_CARGO_HOME).toBe('/home/user/.cargo'); expect(environment.AWF_JAVA_HOME).toBe('/usr/lib/jvm/java-17'); + expect(environment.AWF_BUN_INSTALL).toBe('/home/user/.bun'); } finally { // Restore original values if (originalGoroot !== undefined) { @@ -672,6 +677,11 @@ describe('docker-manager', () => { } else { delete process.env.JAVA_HOME; } + if (originalBunInstall !== undefined) { + process.env.BUN_INSTALL = originalBunInstall; + } else { + delete process.env.BUN_INSTALL; + } } }); @@ -724,7 +734,7 @@ describe('docker-manager', () => { expect(volumes).toContain('/etc/nsswitch.conf:/host/etc/nsswitch.conf:ro'); }); - it('should mount /etc/hosts for hostname resolution in chroot mode', () => { + it('should not mount /etc/hosts in chroot mode (entrypoint copies it at runtime)', () => { const configWithChroot = { ...mockConfig, enableChroot: true @@ -733,8 +743,9 @@ describe('docker-manager', () => { const agent = result.services.agent; const volumes = agent.volumes as string[]; - // /etc/hosts is needed for localhost resolution inside chroot - expect(volumes).toContain('/etc/hosts:/host/etc/hosts:ro'); + // /etc/hosts is NOT mounted — entrypoint copies container's /etc/hosts to /host/etc/hosts + // at runtime to include Docker extra_hosts (host.docker.internal) without modifying host + expect(volumes.some((v: string) => v.includes('/etc/hosts'))).toBe(false); }); it('should use GHCR image when enableChroot is true with default preset (GHCR)', () => { @@ -965,11 +976,13 @@ describe('docker-manager', () => { const result = generateDockerCompose(configWithEnvAll, mockNetworkConfig); const env = result.services.agent.environment as Record; - // Proxy vars must NOT leak from host (intercept mode handles routing) + // HTTP_PROXY must NOT leak from host (intercept mode handles HTTP routing) expect(env.HTTP_PROXY).toBeUndefined(); - expect(env.HTTPS_PROXY).toBeUndefined(); expect(env.http_proxy).toBeUndefined(); + // https_proxy (lowercase) must NOT leak from host — AWF sets HTTPS_PROXY explicitly expect(env.https_proxy).toBeUndefined(); + // HTTPS_PROXY is set by AWF (not from host) for CONNECT tunneling + expect(env.HTTPS_PROXY).toBe('http://172.30.0.10:3128'); } finally { if (originalHttpProxy !== undefined) { process.env.HTTP_PROXY = originalHttpProxy; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 5c957c0a..6d360d49 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -319,16 +319,16 @@ export function generateDockerCompose( 'SUDO_USER', // Sudo metadata 'SUDO_UID', // Sudo metadata 'SUDO_GID', // Sudo metadata - 'HTTP_PROXY', // Intercept mode handles routing; explicit proxy is unreachable - 'HTTPS_PROXY', // Intercept mode handles routing; explicit proxy is unreachable + 'HTTP_PROXY', // Intercept mode handles HTTP routing via iptables DNAT 'http_proxy', // Lowercase variant - 'https_proxy', // Lowercase variant + 'https_proxy', // Lowercase variant — AWF sets HTTPS_PROXY explicitly; prevent host conflicts ]); // Start with required/overridden environment variables // For chroot mode, use the real user's home (not /root when running with sudo) const homeDir = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root'); const environment: Record = { + HTTPS_PROXY: `http://${networkConfig.squidIp}:${SQUID_PORT}`, SQUID_PROXY_HOST: 'squid-proxy', SQUID_PROXY_PORT: SQUID_PORT.toString(), SQUID_INTERCEPT_PORT: SQUID_INTERCEPT_PORT.toString(), @@ -355,6 +355,10 @@ export function generateDockerCompose( if (process.env.JAVA_HOME) { environment.AWF_JAVA_HOME = process.env.JAVA_HOME; } + // Bun: Pass BUN_INSTALL so entrypoint can add $BUN_INSTALL/bin to PATH + if (process.env.BUN_INSTALL) { + environment.AWF_BUN_INSTALL = process.env.BUN_INSTALL; + } } // If --env-all is specified, pass through all host environment variables (except excluded ones) @@ -471,7 +475,9 @@ export function generateDockerCompose( '/etc/passwd:/host/etc/passwd:ro', // User database (needed for getent/user lookup) '/etc/group:/host/etc/group:ro', // Group database (needed for getent/group lookup) '/etc/nsswitch.conf:/host/etc/nsswitch.conf:ro', // Name service switch config - '/etc/hosts:/host/etc/hosts:ro', // Host name resolution (localhost, etc.) Note: won't include Docker extra_hosts like host.docker.internal + // Note: /etc/hosts is NOT mounted here. The entrypoint copies the container's /etc/hosts + // (which includes Docker extra_hosts like host.docker.internal) to /host/etc/hosts at runtime. + // This avoids modifying the host's actual /etc/hosts file. ); // SECURITY: Hide Docker socket to prevent firewall bypass via 'docker run'