agent-first-http 0.5.0

Give an AI agent any URL and get back a usable page — fetched directly, or rendered in a real browser when the page needs one — with a human able to take over the same browser for a login, captcha, or 2FA.
Documentation
# Deployment

`afhttp` has two roles, and they deploy differently:

- The **driver** (`afhttp fetch` / `cdp` / `upload` / `ui` / `health` /
  `capabilities` / `tabs`) is a thin client. Install it wherever the agent runs —
  no container needed.
- The **host** (`afhttp host`) runs a real browser, holds a persistent profile,
  and exposes a CDP endpoint. **Run the host in a container.** That is the
  supported deployment, and this spore ships one at
  [`container/docker/`]../container/docker/.

## Why the host belongs in a container

afhttp keeps Chromium's OS sandbox **on by default** — running unsandboxed
against untrusted, often adversarial pages is the weakest posture, so it must be
opt-in, not the default. The shipped image sets `AFHTTP_NO_SANDBOX=1`, which
makes the host launch Chromium with `--no-sandbox --disable-setuid-sandbox`
(Chromium's own sandbox can't initialize as root without user namespaces anyway)
— **the container then is the isolation boundary.** Run `afhttp host` natively
only on a trusted host; there the sandbox stays on (set `AFHTTP_NO_SANDBOX=1`
yourself only in an environment where Chromium's sandbox can't start). The host
loads untrusted web content and holds live cookies/sessions, so it should run
isolated and disposable:

- non-root (the image already runs as an unprivileged `afhttp` user),
- the default seccomp profile and dropped capabilities,
- `--shm-size=1g` (or a `/dev/shm` mount) for Chromium,
- on a private network — never host networking with the port exposed publicly.

## Quick start

The driver embeds the host image recipe, so a brew-only install (no source tree)
can stand up a host in one command:

```bash
# Build the image if needed and run the host. Auto-detects Docker or Apple
# `container`; add optional backends with repeated --with (see below).
afhttp container install

# Show the running host, its endpoint, and a ready-to-run driver command:
afhttp container status

# Tail logs / tear it down (--purge also drops the image and build cache):
afhttp container logs -f
afhttp container uninstall --purge
```

`install` does **not** compile afhttp: it builds the canonical
`container/docker/Dockerfile` with `--build-arg AFHTTP_BIN_FROM=downloader`,
selecting a stage that **downloads the prebuilt release binary** matching the
driver's own version and the image architecture (`x86_64-unknown-linux-gnu` for
Docker on Intel, `aarch64-unknown-linux-gnu` for Apple `container` and arm64
Docker). BuildKit skips the unused `builder` (Rust) stage, so the image stays a
slim `debian:bookworm-slim` with no toolchain and needs no source tree. The
version is hard-pinned: if no release asset exists for this version/arch (e.g. a
dev build of an unreleased version), the build fails with a pointer to the
from-source path below — it never installs a different version.

**Runtime selection** is `--runtime docker|podman|apple` (auto-detected in that
order: `docker`, then `podman`, then Apple `container`), or the
`AFHTTP_CONTAINER_RUNTIME` env var. Podman behaves like Docker (rootless, no
daemon); Apple `container` builds `linux/arm64` and is started for you
(`container system start`) and has no compose, so on macOS the `afhttp container`
path — not compose — is how you run a host.

**From a source checkout** (development, or to run an unreleased version that has
no published release asset), pass `--from-source`: instead of downloading the
prebuilt binary, it builds the full `container/docker/Dockerfile` from the current
directory (or `--context <dir>`). This works under any runtime — the Dockerfile is
runtime-agnostic — giving a 2×2 of {prebuilt, from-source} × {docker/podman, apple}:

```bash
afhttp container install                              # prebuilt, auto runtime
afhttp container install --from-source                # source build, auto runtime
afhttp container install --runtime apple --from-source  # source build under Apple
```

Caveat for `--from-source` **under Apple `container`**: its builder runs in a
separate persistent VM that defaults to **2 GiB**, and compiling afhttp pulls
`chromiumoxide` (its CDP-bindings crate needs several GB for one `rustc`), so the
build OOM-kills at the default size. The `-m` flag on `container build` does **not**
resize that VM — you must resize the builder itself once (**8 GiB is enough**; the
2 GiB default is not):

```bash
container builder stop && container builder delete
container builder start --cpus 4 --memory 8g
```

After that, from-source builds and runs fine under Apple `container`. (The prebuilt
download path never compiles, so it is unaffected and needs no resizing.)

Or drive the runtime CLI directly (Apple's `container` is docker-shaped, so the
same Dockerfile builds under it with `--platform linux/arm64`):

```bash
cd spores/agent-first-http

# Build the host image (chromium only; build context is the spore root):
docker build -t afhttp-host -f container/docker/Dockerfile .

# Run it. The entrypoint generates a bearer token on first start and prints it
# along with a ready-to-run driver command. The profile persists in the volume.
docker run --rm -p 9222:9222 --shm-size=1g -v afhttp-profile:/data afhttp-host

# …or with compose (toggles backends via WITH_* env, see below):
docker compose -f container/docker/compose.yaml up --build
```

Then, from wherever the agent runs (the driver needs no container):

```bash
afhttp fetch https://example.com \
  --endpoint-url ws://<host>:9222 --token-secret "<token-from-host-logs>"
```

## Security

The CDP endpoint is **full control of the browser and its profile** (cookies,
live sessions, downloads), so the container is **token-by-default**: if you don't
pass `AFHTTP_TOKEN`, the entrypoint generates one and persists it to the profile
volume (`/data/afhttp/host-token`). Set `AFHTTP_TOKEN` yourself to pin it.

afhttp does **not** terminate TLS. For cross-host use, keep the endpoint on a
private network and reach it as `wss://` through a mesh/proxy that provides TLS.
Never expose a tokenless endpoint on a public interface.

## Optional backends (build args)

Chromium is always present (backends `auto` / `chromium`). The rest are opt-in at
build time and arch-guarded (several upstreams ship x86_64-only Linux builds):

| Build arg | Adds backend(s) | Arch |
| --- | --- | --- |
| `WITH_CHROME_HEADLESS_SHELL=1` | `chrome_shell` | x86_64 |
| `WITH_LIGHTPANDA=1` | `lightpanda` | x86_64 + arm64 |
| `WITH_FINGERPRINT_CHROMIUM=1` | `fingerprint_chromium` | x86_64 |
| `WITH_CAMOUFOX=1` | `camoufox` (+ foxbridge) | x86_64 + arm64 |
| `WITH_KASMVNC=1` | real-display takeover (`--takeover kasmvnc`) | x86_64 + arm64 |

```bash
docker build -t afhttp-host:stealth \
  --build-arg WITH_CAMOUFOX=1 --build-arg WITH_FINGERPRINT_CHROMIUM=1 \
  -f container/docker/Dockerfile .
```

The pinned versions live in one place — `container/docker/install-backends.sh` —
shared with the test image (`tests/Dockerfile.test`) so they cannot drift.

### Proprietary browsers (Chrome / Edge / Brave)

These can't be redistributed, so they're not bundled. Mount the vendor binary into
the container and point at it:

```bash
docker run --rm -p 9222:9222 --shm-size=1g \
  -v /opt/google/chrome:/opt/google/chrome:ro \
  afhttp-host --browser chrome --browser-bin /opt/google/chrome/chrome
```

(`--browser chrome` also works when the binary is already on `PATH`.)

## Human takeover

The default ops panel (CDP screencast) needs no X or VNC and works in the slim
image — `afhttp ui --endpoint-url … --token-secret …` prints its URL. Real-display takeover
for hard captcha/IME sites needs `--build-arg WITH_KASMVNC=1`, then start the host
with `--takeover kasmvnc --display headful`. See
[architecture.md §9](architecture.md).

## Lifecycle

`afhttp host` is a single long-running foreground process; it manages its own
browser (and, with KasmVNC, its own `Xvnc`) — so there is no supervisor inside
the image. Process lifecycle (restart policy, scaling, scheduling) stays the
operator's responsibility, exactly as the rest of the contract assumes.