Skip to content

Rootless AWF install (network-isolation) fails on standard runners: writes to root-owned /usr/local #41309

Description

@lpcox

Summary

In network-isolation (rootless) mode, actions/setup/sh/install_awf_binary.sh --rootless fails on standard GitHub-hosted runners (ubuntu-latest). The script installs into the root-owned /usr/local/{bin,lib} prefix, and the rootless preflight aborts because /usr/local/lib/awf cannot be created without sudo:

Installing awf with checksum verification (version: v0.27.10, os: Linux, arch: x86_64)
ERROR: --rootless requires write access to /usr/local/lib/awf
       This directory is root-owned on standard runners. --rootless is intended
       only for ARC/Kubernetes containers where the install dirs are pre-chowned
       to the runner user.
##[error]Process completed with exit code 1.

The --rootless path (emitted by generateAWFInstallationStep when sandbox.agent.network-isolation: true) was written assuming ARC/Kubernetes runners where /usr/local/* is pre-chowned to the runner user. On standard hosted runners that assumption does not hold, so any network-isolation workflow that installs the published awf binary fails at the install step.

Reproduction

A workflow_dispatch workflow with:

sandbox:
  agent:
    network-isolation: true
    version: v0.27.10

…compiles to an install step install_awf_binary.sh v0.27.10 --rootless which fails as above on ubuntu-latest.

(Observed in github/gh-aw-firewall CI; the failing run installed fine only after the fix below.)

Root cause

actions/setup/sh/install_awf_binary.sh:

  • AWF_INSTALL_DIR="/usr/local/bin" and AWF_LIB_DIR="/usr/local/lib/awf" are used for all modes, including --rootless.
  • The rootless preflight only checks writability and errors out, rather than choosing a writable location.
  • The bundle wrapper hardcodes exec ${node_bin} /usr/local/lib/awf/awf-bundle.js, ignoring AWF_LIB_DIR.
  • Nothing adds the install dir to $GITHUB_PATH, so even a successful rootless install in a non-standard dir would not be discoverable by the later bare awf invocation (GetAWFCommandPrefix returns bare awf in network-isolation mode).

Proposed fix

Install the rootless binary into a per-user, always-writable prefix ($HOME/.local/{bin,lib/awf}) and export the bin dir to $GITHUB_PATH. Legacy (non-rootless) behavior is unchanged. This works on both standard hosted runners and ARC/K8s (no pre-chown assumption).

A validated reference implementation is on branch fix/rootless-awf-install-user-prefix (commit c2d22aa3ac):

 actions/setup/sh/install_awf_binary.sh      | 50 ++++++++++++++++++++---------
 pkg/workflow/copilot_engine_installation.go |  3 +-

Specific changes in actions/setup/sh/install_awf_binary.sh

  1. Retarget install dirs in rootless mode (after flag parsing):

    if [ "$ROOTLESS" = "true" ]; then
      AWF_USER_PREFIX="${HOME}/.local"
      AWF_INSTALL_DIR="${AWF_USER_PREFIX}/bin"
      AWF_LIB_DIR="${AWF_USER_PREFIX}/lib/awf"
    fi
  2. Rootless preflight: create + verify the user dirs instead of erroring on /usr/local:

    if [ "$ROOTLESS" = "true" ]; then
      if ! { mkdir -p "${AWF_INSTALL_DIR}" 2>/dev/null && [ -w "${AWF_INSTALL_DIR}" ]; }; then
        echo "ERROR: --rootless could not create a writable install directory at ${AWF_INSTALL_DIR}" >&2
        exit 1
      fi
      if ! { mkdir -p "${AWF_LIB_DIR}" 2>/dev/null && [ -w "${AWF_LIB_DIR}" ]; }; then
        echo "ERROR: --rootless could not create a writable lib directory at ${AWF_LIB_DIR}" >&2
        exit 1
      fi
    fi
  3. Fix the bundle wrapper to use ${AWF_LIB_DIR} instead of the hardcoded path:

    exec ${node_bin} ${AWF_LIB_DIR}/awf-bundle.js "\$@"
  4. Expose the install dir to the current shell and subsequent steps (after install, before the --version check):

    if [ "$ROOTLESS" = "true" ]; then
      export PATH="${AWF_INSTALL_DIR}:${PATH}"
      if [ -n "${GITHUB_PATH:-}" ]; then
        echo "${AWF_INSTALL_DIR}" >> "${GITHUB_PATH}"
      fi
    fi

pkg/workflow/copilot_engine_installation.go

Comment-only update to reflect that --rootless now installs into $HOME/.local/{bin,lib} and exports $GITHUB_PATH (no behavior change; the step still emits install_awf_binary.sh <version> --rootless).

Validation

  • Ran install_awf_binary.sh v0.27.10 --rootless locally with a temp $HOME: installs to $HOME/.local/bin/awf + $HOME/.local/lib/awf/awf-bundle.js, awf --version0.27.10, $GITHUB_PATH updated, zero sudo.
  • go build ./... OK; TestNetworkIsolationRootless passes.
  • End-to-end: a network-isolation workflow_dispatch smoke test in github/gh-aw-firewall went green with this branch pinned (--gh-aw-ref fix/rootless-awf-install-user-prefix) — both "Install AWF binary" and "Execute GitHub Copilot CLI" succeeded, fully rootless, egress enforced.

Acceptance criteria

  • install_awf_binary.sh --rootless succeeds on standard ubuntu-latest runners (no sudo, no pre-chowned /usr/local).
  • Installed awf is resolvable by the bare awf invocation in later steps (via $GITHUB_PATH).
  • Both the lightweight bundle and the platform-binary fallback honor the rootless install dir.
  • Legacy (non network-isolation) install behavior is unchanged.
  • Still works on ARC/Kubernetes runners.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions