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
95 changes: 95 additions & 0 deletions docs/arc-dind.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,101 @@ Language SDKs (Go, Node, Java, .NET) are NOT baked into the sysroot image. They
- run: echo "RUNNER_TOOL_CACHE=/tmp/gh-aw/tool-cache" >> "$GITHUB_ENV"
```

## Staging additional CLI tools (not just the invoking engine binary)

AWF never discovers or stages arbitrary tools that a sandboxed command invokes
(for example a threat-detection binary, a linter, or any helper CLI installed
by a separate workflow step). The only automatic staging is narrow:

- **First-command staging** copies the *first token* of the AWF command (the
invoking engine binary — `copilot`, `claude`, `codex`, …) into a
daemon-visible staging root and mounts it at `/tmp/awf-runner-bin/<name>`.
It runs **only when `--docker-host-path-prefix` is a `/tmp`-rooted path**
shared by the runner and the DinD daemon (for example `/tmp/gh-aw`). A
`/host`-style prefix does **not** trigger it, so with that prefix even the
engine binary must be staged explicitly.
- **`dind.stageEngineBinary`** is an explicitly configured fallback (you give
it `path`/`targetPath`), not automatic discovery.

On `runner.topology: arc-dind`, a tool installed only onto the **runner's**
filesystem (via `actions/setup-*`, a manual `curl` install, or an action that
drops a binary under `$RUNNER_TOOL_CACHE`/`/opt`/`/usr/local/bin`) is invisible
to the sysroot the agent chroots into: that sysroot comes from the
`build-tools` image plus daemon-visible bind mounts, not from the runner pod's
filesystem. Calling such a tool inside AWF without staging it fails with exit
code 127 ("command not found").

### Staging recipe

Copy the tool into a shared, daemon-visible `/tmp`-rooted directory **and
mount that directory**, because staging alone does not expose a path —
`--docker-host-path-prefix` only rewrites bind-mount *sources*, it does not
publish arbitrary runner paths into the sandbox.

```yaml
- name: Stage my-tool and its output directory
run: |
mkdir -p /tmp/gh-aw/bin /tmp/gh-aw/my-tool
cp "$(command -v my-tool)" /tmp/gh-aw/bin/my-tool
chmod +x /tmp/gh-aw/bin/my-tool

- name: Run my-tool under AWF
run: |
sudo awf --docker-host-path-prefix /tmp/gh-aw \
--mount /tmp/gh-aw/bin:/tmp/gh-aw/bin:ro \
--mount /tmp/gh-aw/my-tool:/tmp/gh-aw/my-tool:rw \
--allow-domains api.example.com \
-- /tmp/gh-aw/bin/my-tool --output /tmp/gh-aw/my-tool/result.json
```

Notes on this recipe:

- **Every `--mount` host path must already exist** — AWF validates it before
launch and aborts with `Host path does not exist` otherwise. Create both the
staged bin directory and the output directory in the preceding step.
- **Copying the file returned by `command -v` is only sufficient for a
self-contained executable** (a static binary, or one whose interpreter,
shared libraries, Node/Python modules and data files already exist in the
sysroot). Wrapper scripts, `setup-*`-installed SDK tools, and
package-managed linters are typically shims over a runtime and support tree,
so copying the entry point alone still fails — with a missing-interpreter,
missing-`.so`, or missing-module error rather than exit 127. For those,
stage/mount the whole install tree (and its runtime, e.g. the tool cache at
`/tmp/gh-aw/tool-cache`) instead of a single file.
- **`chroot.binariesSourcePath`** is the alternative when you want staged
tools on `PATH`: AWF mounts that runner-side directory at
`/tmp/awf-runner-bin` inside the chroot and prepends it to `PATH`, so the
command can be invoked by bare name.

### Output paths must be writable — and consistent

Staging the binary only fixes exit code 127. A tool that writes output also
needs its `--output`/destination path to resolve to a mount that is:

1. **writable** — a `:ro` mount used to expose staged inputs does not become
writable just because the tool's output path lives underneath it. Add a
more specific `:rw` mount for the output subdirectory, as in the recipe
above:

```bash
--mount /tmp/gh-aw/bin:/tmp/gh-aw/bin:ro \
--mount /tmp/gh-aw/my-tool:/tmp/gh-aw/my-tool:rw
```

Docker orders bind mounts by destination depth, so the more specific `rw`
mount of the subdirectory is applied after (and therefore takes precedence
over) a broader `ro` mount of its parent.

2. **the same path for the producer and the consumer** — pick one canonical
sandbox path (e.g. always `/tmp/gh-aw/my-tool/result.json`) and use it for
both the AWF-side write and the later step or artifact-upload read. AWF
mounts custom targets under the chroot, so the in-sandbox path matches the
`--mount` container path; the runner-side source is what
`--docker-host-path-prefix` rewrites. A mismatch (writing under
`${RUNNER_TEMP}/…` but reading `/tmp/gh-aw/…`, which
`--docker-host-path-prefix` does **not** alias) produces no error — the
tool "succeeds" but the output never appears where the consumer looks.

## Writable home under sysroot staging

Sysroot staging drops agent bind mounts whose sources the DinD daemon cannot
Expand Down
39 changes: 39 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,45 @@ container:

> **See also:** [docs/arc-dind.md](arc-dind.md) for a complete ARC/DinD configuration guide, including sysroot staging, tool-cache guidance, and end-to-end examples.

### `--mount` and read-only vs. writable output paths

A `--mount <host_path>:<container_path>[:ro|rw]` entry only grants the access
level given for **that specific mount**. Mounting a directory `:ro` so a tool
can read staged inputs from it does **not** make a subdirectory of that same
path writable — Docker/runc treats each `--mount` as an independent bind
mount, so a tool that tries to write its `--output` (or any other file) inside
a `:ro`-mounted tree will fail even though the path "looks" present.

If a tool needs to both read from and write under a shared parent directory
(a common pattern for ARC/DinD staging under `${RUNNER_TEMP}/gh-aw`), mount
the writable subpath explicitly as `:rw` in addition to the `:ro` parent:

```bash
# Every --mount host path must exist first: AWF aborts with
# "Host path does not exist" before the command runs otherwise.
mkdir -p /tmp/gh-aw/inputs /tmp/gh-aw/my-tool

awf --mount /tmp/gh-aw/inputs:/tmp/gh-aw/inputs:ro \
--mount /tmp/gh-aw/my-tool:/tmp/gh-aw/my-tool:rw \
--allow-domains github.com \
-- my-tool --output /tmp/gh-aw/my-tool/result.json
```

When one mount is nested inside another, Docker applies bind mounts in
destination order, so the more specific `rw` mount for the subdirectory takes
precedence over a broader `ro` mount of its ancestor.

Also double-check that the path a tool **writes** to and the path a later
step (or artifact-upload action) **reads** from are the same path once
`--docker-host-path-prefix` translation is applied — a producer/consumer path
mismatch (e.g. writing under `${RUNNER_TEMP}/gh-aw/...` but reading from
`/tmp/gh-aw/...`, which `--docker-host-path-prefix` does not alias) fails
silently: no error is raised, but the expected output file never appears where
the consumer looks for it. See
[docs/arc-dind.md](arc-dind.md#staging-additional-cli-tools-not-just-the-invoking-engine-binary)
for the full staging + rw-mount pattern, using a threat-detection tool as a
worked example.

### Security: procfs and credential isolation

AWF mounts a container-scoped procfs at `/host/proc` with `hidepid=2` to prevent the agent from reading other processes' environment variables. This is critical because:
Expand Down
7 changes: 7 additions & 0 deletions docs/selective-mounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ awf --mount /etc/custom:/etc/custom:ro --allow-domains github.com -- cat /etc/cu
```bash
awf --mount /data:/data:ro --allow-domains github.com -- process-data
```
If a tool needs to write output under a directory you otherwise mount
`:ro` (for example a shared staging directory on ARC/DinD runners), add a
separate, more specific `:rw` mount for just that output subpath — a
`:ro` mount of a parent directory never grants write access to paths
underneath it. See [ARC + DinD Configuration](arc-dind.md#staging-additional-cli-tools-not-just-the-invoking-engine-binary)
and [`--mount` guidance](environment.md#--mount-and-read-only-vs-writable-output-paths)
for the full pattern.

3. **Minimize mounted directories** - Only mount what's needed:
```bash
Expand Down
Loading