Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -708,11 +708,12 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then

# Find the user name on the host system by UID
# This allows us to run as the same user inside the chroot
HOST_USER_UID="${AWF_USER_UID:-1000}"
HOST_USER_GID="${AWF_USER_GID:-${HOST_USER_UID}}"
HOST_USER_UID="${AWF_CHROOT_IDENTITY_UID:-${AWF_USER_UID:-1000}}"
HOST_USER_GID="${AWF_CHROOT_IDENTITY_GID:-${AWF_USER_GID:-${HOST_USER_UID}}}"
HOST_USER=$(chroot /host getent passwd "${HOST_USER_UID}" 2>/dev/null | cut -d: -f1 || echo "")
CAPSH_IDENTITY_ARGS=""
CHROOT_HOME_OVERRIDE=""
CHROOT_HOME_OVERRIDE="${AWF_CHROOT_IDENTITY_HOME:-}"
CHROOT_USER_OVERRIDE="${AWF_CHROOT_IDENTITY_USER:-}"
if [ -z "${HOST_USER}" ]; then
# User not found in chroot's /etc/passwd (common on ARC-DinD Alpine daemons).
# Synthesize minimal identity files so the agent can resolve its own UID/GID.
Expand Down Expand Up @@ -793,14 +794,20 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
echo "[entrypoint] Running as synthesized host user: ${HOST_USER} (UID: ${HOST_USER_UID})"
else
CAPSH_IDENTITY_ARGS="--gid=${HOST_USER_GID} --uid=${HOST_USER_UID} --groups=${HOST_USER_GID}"
CHROOT_HOME_OVERRIDE="${SYNTH_HOME}"
if [ -z "${CHROOT_HOME_OVERRIDE}" ]; then
CHROOT_HOME_OVERRIDE="${SYNTH_HOME}"
fi
echo "[entrypoint][WARN] Proceeding with numeric UID/GID fallback (${HOST_USER_UID}:${HOST_USER_GID})"
fi
else
CAPSH_IDENTITY_ARGS="--user=${HOST_USER}"
echo "[entrypoint] Running as host user: ${HOST_USER} (UID: ${HOST_USER_UID})"
fi

if [ -z "${CHROOT_USER_OVERRIDE}" ] && [ -n "${HOST_USER}" ]; then
CHROOT_USER_OVERRIDE="${HOST_USER}"
fi

# Write the command to a temporary script file in the chroot
# This avoids complex quoting issues with nested shells
SCRIPT_FILE="/tmp/awf-cmd-$$.sh"
Expand Down Expand Up @@ -1055,11 +1062,14 @@ AWFEOF
LD_PRELOAD_CMD="export LD_PRELOAD=${ONE_SHOT_TOKEN_LIB};"
fi

AWF_CHROOT_EFFECTIVE_HOME="${CHROOT_HOME_OVERRIDE}" \
AWF_CHROOT_EFFECTIVE_USER="${CHROOT_USER_OVERRIDE}" \
run_agent_with_token_protection chroot /host /bin/bash -c "
cd '${CHROOT_WORKDIR}' 2>/dev/null || cd /
trap '${CLEANUP_CMD}' EXIT
${LD_PRELOAD_CMD}
if [ -n '${CHROOT_HOME_OVERRIDE}' ]; then export HOME='${CHROOT_HOME_OVERRIDE}'; fi
if [ -n \"\${AWF_CHROOT_EFFECTIVE_HOME:-}\" ]; then export HOME=\"\${AWF_CHROOT_EFFECTIVE_HOME}\"; fi
if [ -n \"\${AWF_CHROOT_EFFECTIVE_USER:-}\" ]; then export USER=\"\${AWF_CHROOT_EFFECTIVE_USER}\"; export LOGNAME=\"\${AWF_CHROOT_EFFECTIVE_USER}\"; fi
exec capsh --drop=${CAPS_TO_DROP} ${CAPSH_IDENTITY_ARGS} -- -c 'exec ${SCRIPT_FILE}'
Comment on lines 1070 to 1073
"
else
Expand Down
75 changes: 53 additions & 22 deletions docs/arc-dind.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,60 @@
# ARC + DinD notes

When using ARC runners with a split runner/daemon filesystem (`DOCKER_HOST` sidecar) and `--docker-host-path-prefix`, AWF now stages required chroot files automatically:

- invoking CLI binary (for example `copilot`, `claude`, `codex`)
- `/etc/passwd`
- `/etc/group`
- chroot `/etc/hosts`

AWF validates the staged runner binary name before using it in chroot bootstrap paths. Per-run staged chroot-host directories remain unique and AWF prunes stale ones automatically from the shared staging root.
# ARC + DinD Configuration

AWF supports ARC runners where the runner filesystem and Docker daemon filesystem are split (DinD sidecar patterns).

## What AWF now handles automatically

- Split-filesystem probing for `--docker-host-path-prefix`
- Chroot staging for:
- invoking CLI binary (`copilot`, `claude`, `codex`, etc.)
- `/etc/passwd`
- `/etc/group`
- generated chroot `/etc/hosts`
- DinD `DOCKER_HOST` propagation into agent/MCP environments when DinD is detected

## ARC/DinD stdin config surface

```json
{
"container": {
"enableDind": true,
"dockerHostPathPrefix": "/tmp/gh-aw"
},
"chroot": {
"identity": {
"home": "/tmp/gh-aw/home",
"user": "runner",
"uid": 1001,
"gid": 1001
}
},
"dind": {
"preStageDirs": true,
"workDir": "/tmp/gh-aw",
"stagingImage": "ghcr.io/github/gh-aw-firewall/agent:latest",
"stageEngineBinary": {
"path": "/usr/local/bin/copilot",
"targetPath": "/usr/local/bin/copilot"
}
}
}
```

## Field behavior

- `chroot.identity.*`: applied inside entrypoint **after** `chroot /host` to override HOME/USER/LOGNAME and identity mapping hints.
- `dind.preStageDirs`: runs a short-lived staging container in DinD mode to create required workdir tree with open permissions.
- `dind.stageEngineBinary`: copies an engine binary from the runner path into daemon-visible filesystem before compose startup.
- `dind.stagingImage`: image used for short-lived staging containers.
- `dind.workDir`: target root for DinD pre-staged directory tree (`/tmp/gh-aw` default).

## Auto-detection of split filesystem setups

AWF detects likely ARC/DinD environments at startup and warns when `--docker-host-path-prefix` is missing:

- **Non-standard `DOCKER_HOST` unix socket**: any `unix://` socket outside `/var/run/docker.sock` and `/run/docker.sock` is treated as a sibling-daemon pod indicator.
- **`AWF_DIND=1`**: operators can set this environment variable to explicitly declare a DinD setup.

When either signal is present and no explicit prefix is supplied, AWF emits a warning suggesting `--docker-host-path-prefix` (for example, `--docker-host-path-prefix /tmp/gh-aw` for typical ARC layouts). The DinD probe also considers `/tmp/gh-aw` as a candidate prefix when discovering the split-filesystem layout.

## Remaining requirement: Node.js in the DinD-visible host filesystem

Copilot CLI still requires `node` to be available inside the chrooted runtime PATH. Ensure your DinD image (or staged host toolcache) includes Node.js.

Recommended base image for ARC DinD sidecars:
- non-default unix `DOCKER_HOST` socket paths (outside `/var/run/docker.sock` and `/run/docker.sock`)
- `AWF_DIND=1`

- `node:20-bookworm`
## Runtime prerequisite

This provides a glibc userspace compatible with AWF chroot mode plus a current Node.js runtime.
Copilot CLI still requires `node` to be available inside the chrooted runtime PATH.
11 changes: 11 additions & 0 deletions docs/awf-config-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,15 @@ the corresponding CLI flag.
- `container.dockerHost` → `--docker-host`
- `container.dockerHostPathPrefix` → `--docker-host-path-prefix`
- `container.runnerToolCachePath` → *(config-only; checked first for optional read-only runner tool cache mount, before `RUNNER_TOOL_CACHE` and `/home/runner/work/_tool` auto-detection)*
- `chroot.identity.home` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_HOME` and applied after chroot pivot)*
- `chroot.identity.user` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_USER` and applied to `USER`/`LOGNAME` after chroot pivot)*
- `chroot.identity.uid` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_UID` for chroot user mapping)*
- `chroot.identity.gid` → *(config-only; forwarded as `AWF_CHROOT_IDENTITY_GID` for chroot user mapping)*
- `dind.preStageDirs` → *(config-only; enables daemon-side pre-staging of the DinD work directory tree before compose startup)*
- `dind.workDir` → *(config-only; daemon-visible staging root, default `/tmp/gh-aw`)*
- `dind.stagingImage` → *(config-only; image used for short-lived DinD staging containers)*
- `dind.stageEngineBinary.path` → *(config-only; runner-side engine binary source path for DinD staging)*
- `dind.stageEngineBinary.targetPath` → *(config-only; daemon-side destination path for staged engine binary)*
- `environment.envFile` → `--env-file`
- `environment.envAll` → `--env-all`
- `environment.excludeEnv[]` → `--exclude-env` *(repeatable)*
Expand All @@ -177,6 +186,8 @@ the corresponding CLI flag.

When `container.dockerHostPathPrefix` points at a daemon-visible shared `/tmp` path, the implementation stages the invoking CLI binary together with `/etc/passwd`, `/etc/group`, and the generated chroot `/etc/hosts` under that shared path so chroot mode can bootstrap on split-filesystem ARC/DinD hosts.

When DinD is detected, AWF preserves the detected `DOCKER_HOST` value for the agent environment (including MCP servers) so DinD-aware tooling can reach the correct daemon without manual workflow env overrides.

The following CLI flag has no config-file equivalent by design:

- `-e, --env <KEY=VALUE>` — inject a single environment variable into
Expand Down
66 changes: 66 additions & 0 deletions docs/awf-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,72 @@
}
}
},
"chroot": {
"type": "object",
"description": "Chroot execution overrides for split-filesystem ARC/DinD runners.",
"additionalProperties": false,
"properties": {
"identity": {
"type": "object",
"description": "Identity values applied after chroot pivot to override HOME/USER defaults inside chroot mode.",
"additionalProperties": false,
"properties": {
"home": {
"type": "string",
"description": "Home directory path to export inside chroot mode (for example: \"/tmp/gh-aw/home\")."
},
"user": {
"type": "string",
"description": "User/LOGNAME string to export inside chroot mode (for example: \"runner\")."
},
"uid": {
"type": "integer",
"minimum": 1,
"description": "UID hint used for chroot identity synthesis and user switching."
},
Comment on lines +513 to +517
"gid": {
"type": "integer",
"minimum": 1,
"description": "GID hint used for chroot identity synthesis and user switching."
}
Comment on lines +518 to +522
}
}
}
},
"dind": {
"type": "object",
"description": "Bootstrap helpers for ARC/DinD split runner/daemon filesystems.",
"additionalProperties": false,
"properties": {
"preStageDirs": {
"type": "boolean",
"description": "When true and DinD is detected, AWF pre-creates the required /tmp/gh-aw directory tree inside the daemon-visible filesystem before compose startup."
},
"workDir": {
"type": "string",
"description": "Daemon-visible working directory for DinD pre-staging (default: \"/tmp/gh-aw\")."
},
"stagingImage": {
"type": "string",
"description": "Container image used for short-lived DinD staging operations."
},
"stageEngineBinary": {
"type": "object",
"description": "Engine binary staging settings for split-filesystem DinD setups.",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"description": "Runner-side path to the engine binary to stage (for example: \"/usr/local/bin/copilot\")."
},
"targetPath": {
"type": "string",
"description": "Daemon filesystem destination path for the staged engine binary."
}
}
}
}
},
"environment": {
"type": "object",
"description": "Environment variable propagation into the agent container. Merge behavior is: AWF-reserved variables are set by AWF and are not overridden by envAll or envFile; if envAll is true, host environment variables are forwarded next; envFile is then applied only for variables not already present, so it does not override envAll; CLI -e/--env has highest precedence and may override any variable, including AWF-reserved ones. When apiProxy.enabled is true, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are excluded from the agent and held in the API proxy sidecar. See docs/awf-config-spec.md §8–9 for credential isolation rules.",
Expand Down
19 changes: 19 additions & 0 deletions docs/chroot-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,25 @@ AWF handles this automatically at two layers:

No configuration is required — synthesis is triggered automatically when user lookup fails.

### Chroot Identity Override (ARC/DinD)

On split-filesystem ARC/DinD runners, you can explicitly override chroot identity values via stdin config:

```json
{
"chroot": {
"identity": {
"home": "/tmp/gh-aw/home",
"user": "runner",
"uid": 1001,
"gid": 1001
}
}
}
```

AWF forwards these values to the agent entrypoint and applies them **after** `chroot /host`, overriding default `HOME`, `USER`, and `LOGNAME` values for the chrooted command runtime.

### Error: Working directory does not exist

```
Expand Down
66 changes: 66 additions & 0 deletions src/awf-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,72 @@
}
}
},
"chroot": {
"type": "object",
"description": "Chroot execution overrides for split-filesystem ARC/DinD runners.",
"additionalProperties": false,
"properties": {
"identity": {
"type": "object",
"description": "Identity values applied after chroot pivot to override HOME/USER defaults inside chroot mode.",
"additionalProperties": false,
"properties": {
"home": {
"type": "string",
"description": "Home directory path to export inside chroot mode (for example: \"/tmp/gh-aw/home\")."
},
"user": {
"type": "string",
"description": "User/LOGNAME string to export inside chroot mode (for example: \"runner\")."
},
"uid": {
"type": "integer",
"minimum": 1,
"description": "UID hint used for chroot identity synthesis and user switching."
},
Comment on lines +513 to +517
"gid": {
"type": "integer",
"minimum": 1,
"description": "GID hint used for chroot identity synthesis and user switching."
}
Comment on lines +518 to +522
}
}
}
},
"dind": {
"type": "object",
"description": "Bootstrap helpers for ARC/DinD split runner/daemon filesystems.",
"additionalProperties": false,
"properties": {
"preStageDirs": {
"type": "boolean",
"description": "When true and DinD is detected, AWF pre-creates the required /tmp/gh-aw directory tree inside the daemon-visible filesystem before compose startup."
},
"workDir": {
"type": "string",
"description": "Daemon-visible working directory for DinD pre-staging (default: \"/tmp/gh-aw\")."
},
"stagingImage": {
"type": "string",
"description": "Container image used for short-lived DinD staging operations."
},
"stageEngineBinary": {
"type": "object",
"description": "Engine binary staging settings for split-filesystem DinD setups.",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
"description": "Runner-side path to the engine binary to stage (for example: \"/usr/local/bin/copilot\")."
},
"targetPath": {
"type": "string",
"description": "Daemon filesystem destination path for the staged engine binary."
}
}
}
}
},
"environment": {
"type": "object",
"description": "Environment variable propagation into the agent container. Merge behavior is: AWF-reserved variables are set by AWF and are not overridden by envAll or envFile; if envAll is true, host environment variables are forwarded next; envFile is then applied only for variables not already present, so it does not override envAll; CLI -e/--env has highest precedence and may override any variable, including AWF-reserved ones. When apiProxy.enabled is true, source credentials (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.) are excluded from the agent and held in the API proxy sidecar. See docs/awf-config-spec.md §8–9 for credential isolation rules.",
Expand Down
51 changes: 51 additions & 0 deletions src/commands/build-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,57 @@ describe('buildConfig', () => {
expect(config.runnerToolCachePath).toBe('/opt/hostedtoolcache');
});

it('should pass through chroot identity fields', () => {
const config = buildConfig(makeInputs({
options: {
...makeInputs().options,
chrootIdentityHome: '/tmp/gh-aw/home',
chrootIdentityUser: 'runner',
chrootIdentityUid: '1001',
chrootIdentityGid: '1001',
},
}));
expect(config.chrootIdentity).toEqual({
home: '/tmp/gh-aw/home',
user: 'runner',
uid: 1001,
gid: 1001,
});
});

it('should ignore non-positive chroot identity uid/gid values', () => {
const config = buildConfig(makeInputs({
options: {
...makeInputs().options,
chrootIdentityUid: '0',
chrootIdentityGid: '-1',
},
}));
expect(config.chrootIdentity).toBeUndefined();
});

it('should pass through dind bootstrap fields', () => {
const config = buildConfig(makeInputs({
options: {
...makeInputs().options,
dindPreStageDirs: true,
dindWorkDir: '/tmp/gh-aw',
dindStagingImage: 'ghcr.io/github/gh-aw-firewall/agent:latest',
dindStageEngineBinaryPath: '/usr/local/bin/copilot',
dindStageEngineBinaryTargetPath: '/usr/local/bin/copilot',
},
}));
expect(config.dind).toEqual({
preStageDirs: true,
workDir: '/tmp/gh-aw',
stagingImage: 'ghcr.io/github/gh-aw-firewall/agent:latest',
stageEngineBinary: {
path: '/usr/local/bin/copilot',
targetPath: '/usr/local/bin/copilot',
},
});
});

it('should pass through modelAliases', () => {
const aliases = { 'gpt-4': ['gpt-4-turbo'] };
const config = buildConfig(makeInputs({ modelAliases: aliases }));
Expand Down
Loading
Loading