From 44cc9ae1a7aef3cfa3e4a4cd2ca31f4cb5d6f12b Mon Sep 17 00:00:00 2001 From: omar-nahhas-agent Date: Fri, 19 Jun 2026 23:42:10 +0000 Subject: [PATCH 1/6] =?UTF-8?q?spike(devcontainer):=20Phase-0=20feasibilit?= =?UTF-8?q?y=20gate=20=E2=80=94=20rootless=20nested=20A0=20boot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Proves the Critical-4 assumption behind the plugin-quality-standard SPEC: a devkit devcontainer (Playwright + rootless podman) can boot the Agent Zero container NESTED and UNPRIVILEGED (no --privileged, no host docker.sock) and the harness can reach + authenticate against it. - devcontainer/Containerfile Playwright base + rootless podman toolchain - devcontainer/spike/run-acceptance.sh nested boot + login acceptance - .github/workflows/devcontainer-spike.yml runs it on ubuntu-latest Mechanism already confirmed on a real Linux host; this lets the GH runner be the authoritative proof. Ref: SPEC DEC-018/031, Appendix E.6. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/devcontainer-spike.yml | 49 ++++++++++++++ devcontainer/Containerfile | 72 ++++++++++++++++++++ devcontainer/README.md | 48 +++++++++++++ devcontainer/spike/run-acceptance.sh | 85 ++++++++++++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 .github/workflows/devcontainer-spike.yml create mode 100644 devcontainer/Containerfile create mode 100644 devcontainer/README.md create mode 100755 devcontainer/spike/run-acceptance.sh diff --git a/.github/workflows/devcontainer-spike.yml b/.github/workflows/devcontainer-spike.yml new file mode 100644 index 0000000..7827bac --- /dev/null +++ b/.github/workflows/devcontainer-spike.yml @@ -0,0 +1,49 @@ +# Phase-0 feasibility spike (SPEC DEC-031 / Appendix E.6). +# +# Proves on a GitHub-hosted ubuntu-latest runner that the devkit devcontainer +# can boot Agent Zero NESTED via ROOTLESS podman — no --privileged, no host +# docker.sock — and that the harness reaches + authenticates against it. +# +# This is the gate for everything else in the plugin-quality-standard SPEC: +# if this is red, the devcontainer model (DEC-018) needs the self-hosted-runner +# fallback before Phase 1. +name: devcontainer-spike + +on: + workflow_dispatch: + inputs: + a0_image: + description: "A0 image to boot nested (DEC-019 parameter)" + required: false + default: "agent0ai/agent-zero:latest" + push: + branches: ["spike/devcontainer-phase0"] + +jobs: + rootless-nested-boot: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - name: Tooling versions (rootless, no privilege) + run: | + podman --version + echo "rootless: $(podman info --format '{{.Host.Security.Rootless}}')" + ls -l /dev/fuse || true + + - name: Build the devkit devcontainer image (rootless) + run: podman build -t plugin-devkit:spike -f devcontainer/Containerfile devcontainer + + - name: Run Phase-0 acceptance INSIDE the devcontainer (nested, unprivileged) + env: + A0_IMAGE: ${{ github.event.inputs.a0_image || 'agent0ai/agent-zero:latest' }} + run: | + podman run --rm \ + --device /dev/fuse \ + --security-opt label=disable \ + -e A0_IMAGE="$A0_IMAGE" \ + -v "$PWD":/workspace:Z \ + -w /workspace \ + plugin-devkit:spike \ + -lc 'bash devcontainer/spike/run-acceptance.sh' diff --git a/devcontainer/Containerfile b/devcontainer/Containerfile new file mode 100644 index 0000000..2e495f3 --- /dev/null +++ b/devcontainer/Containerfile @@ -0,0 +1,72 @@ +# Agent Zero plugin devkit — unified execution environment. +# +# One image, three uses (SPEC DEC-018): +# 1. GitHub Actions runs the plugin e2e lifecycle inside it. +# 2. A developer uses it as a dev container. +# 3. An AI agent runs it (docker OR podman) to develop + test locally. +# +# It carries Playwright (browsers + deps) AND rootless podman, so it can boot +# the Agent Zero container *nested and unprivileged* — no --privileged, no host +# docker.sock (SPEC REQ-DEV-003 / REQ-CI-006). Proven mechanism: rootless +# podman-in-podman via `--device /dev/fuse --security-opt label=disable`. +# +# Base: the official Playwright image (Ubuntu jammy) already ships node + the +# browser binaries + their system libs — the painful half of the image. + +FROM mcr.microsoft.com/playwright:v1.48.0-jammy + +# --- rootless podman toolchain ------------------------------------------------- +# podman + the rootless plumbing: fuse-overlayfs (rootless storage), uidmap +# (newuidmap/newgidmap for subuid/subgid), slirp4netns (rootless networking). +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + podman \ + fuse-overlayfs \ + uidmap \ + slirp4netns \ + crun \ + curl \ + jq \ + openssh-client \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# --- rootless configuration ---------------------------------------------------- +# The Playwright image ships an unprivileged user `pwuser` (uid 1000). Give it a +# subuid/subgid range so rootless podman can map a container's users. +RUN echo "pwuser:100000:65536" > /etc/subuid \ + && echo "pwuser:100000:65536" > /etc/subgid + +# Storage: overlay backed by fuse-overlayfs (works rootless, no kernel overlay +# mount privilege required). +RUN mkdir -p /etc/containers \ + && printf '%s\n' \ + '[storage]' \ + 'driver = "overlay"' \ + '[storage.options.overlay]' \ + 'mount_program = "/usr/bin/fuse-overlayfs"' \ + > /etc/containers/storage.conf \ + && printf '%s\n' \ + '[engine]' \ + 'cgroup_manager = "cgroupfs"' \ + 'events_logger = "file"' \ + '[containers]' \ + 'netns = "slirp4netns"' \ + > /etc/containers/containers.conf \ + && printf '%s\n' \ + 'unqualified-search-registries = ["docker.io"]' \ + > /etc/containers/registries.conf + +# A short, well-known registries policy so `podman pull docker.io/...` works. +RUN mkdir -p /home/pwuser/.config/containers \ + && chown -R pwuser:pwuser /home/pwuser/.config + +USER pwuser +ENV HOME=/home/pwuser +WORKDIR /workspace + +# Sanity: podman present and reports rootless. (Build-time check; nesting is +# exercised at run time by the spike.) +RUN podman --version + +ENTRYPOINT ["/bin/bash"] diff --git a/devcontainer/README.md b/devcontainer/README.md new file mode 100644 index 0000000..e3c4107 --- /dev/null +++ b/devcontainer/README.md @@ -0,0 +1,48 @@ +# Devkit devcontainer (Phase-0 spike) + +One image, three uses (SPEC `DEC-018`): + +1. **CI** runs the plugin e2e lifecycle inside it. +2. **Developers** use it as a dev container. +3. **AI agents** run it (docker or podman) to develop + test locally before pushing. + +It carries **Playwright** (browsers + system libs, from the official Playwright base) +plus **rootless podman**, so it boots the Agent Zero container **nested and +unprivileged** — no `--privileged`, no host `docker.sock`. + +## Why this exists + +The whole plugin-quality-standard rests on one risky assumption (`Critical-4` in +`SPEC-REVIEW-001.md`): that rootless podman-in-podman works on GitHub-hosted runners. +This directory is the **Phase-0 gate** that proves it before anything else is built. + +The mechanism was confirmed on a real Linux host: + +``` +podman run --device /dev/fuse --security-opt label=disable --user podman \ + quay.io/podman/stable \ + podman run --rm docker.io/library/alpine echo OK # → OK, no --privileged +``` + +## Run it + +Locally (needs rootless podman + `/dev/fuse` + subuid/subgid on the host): + +```bash +podman build -t plugin-devkit:spike -f devcontainer/Containerfile devcontainer +podman run --rm --device /dev/fuse --security-opt label=disable \ + -v "$PWD":/workspace -w /workspace plugin-devkit:spike \ + -lc 'bash devcontainer/spike/run-acceptance.sh' +``` + +In CI: the `devcontainer-spike` workflow runs exactly this on `ubuntu-latest`. + +## Acceptance (`spike/run-acceptance.sh`) + +Passes iff, unprivileged: (1) a nested rootless container runs, (2) the real A0 image +boots nested and `/login` becomes healthy, (3) the harness logs in and gets an +authenticated response. The full `install → verify-installed → uninstall → +verify-uninstalled` cycle layers on top via the existing Playwright harness (Phase 2). + +> Status: spike. The image pins `mcr.microsoft.com/playwright:v1.48.0-jammy`; bump in +> lockstep with the harness's Playwright version when this graduates out of spike. diff --git a/devcontainer/spike/run-acceptance.sh b/devcontainer/spike/run-acceptance.sh new file mode 100755 index 0000000..d320f52 --- /dev/null +++ b/devcontainer/spike/run-acceptance.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Phase-0 feasibility acceptance (SPEC DEC-031 / Appendix E.6). +# +# Runs INSIDE the devkit devcontainer. Proves the gating assumption behind the +# whole standard: that the Agent Zero container can be booted *nested* inside +# this container by *rootless* podman — no --privileged, no host docker.sock — +# and that the harness can reach + authenticate against it. +# +# Pass criteria (the Critical-4 gate): +# 1. rootless podman runs nested (a throwaway alpine container runs) +# 2. the real A0 image boots nested and /login becomes healthy +# 3. the harness logs in and gets an authenticated response +# The full install/verify/uninstall cycle runs on top of this via the existing +# Playwright harness (proven on livekit) — it is Phase-2, not this gate. +set -euo pipefail + +A0_IMAGE="${A0_IMAGE:-agent0ai/agent-zero:latest}" # DEC-019: parameterized +A0_HTTP_PORT="${A0_HTTP_PORT:-8080}" +A0_NAME="a0-spike" +AUTH_LOGIN="${AUTH_LOGIN:-admin}" +AUTH_PASSWORD="${AUTH_PASSWORD:-admin}" +BOOT_TIMEOUT="${BOOT_TIMEOUT:-300}" +BASE="http://localhost:${A0_HTTP_PORT}" +COOKIES="$(mktemp)" + +log(){ printf '\n\033[1;36m[spike]\033[0m %s\n' "$*"; } +fail(){ printf '\n\033[1;31m[spike FAIL]\033[0m %s\n' "$*"; exit 1; } + +cleanup(){ podman rm -f "$A0_NAME" >/dev/null 2>&1 || true; rm -f "$COOKIES"; } +trap cleanup EXIT + +# --- 0. environment sanity ---------------------------------------------------- +log "podman: $(podman --version) | rootless: $(podman info --format '{{.Host.Security.Rootless}}' 2>/dev/null || echo '?')" + +# --- 1. nested rootless podman can run a container at all --------------------- +log "1/3 nested rootless container smoke test" +podman run --rm docker.io/library/alpine:3.20 echo NESTED_OK | grep -q NESTED_OK \ + || fail "rootless podman could not run a nested container" +log " ✓ nested rootless container ran" + +# --- 2. boot the real A0 image nested ----------------------------------------- +log "2/3 booting A0 nested: $A0_IMAGE on :$A0_HTTP_PORT" +podman run -d --name "$A0_NAME" \ + -p "${A0_HTTP_PORT}:80" \ + -e AUTH_LOGIN="$AUTH_LOGIN" -e AUTH_PASSWORD="$AUTH_PASSWORD" \ + -e A0_SEEDED_SETTINGS_JSON='{}' \ + -e BRANCH=main \ + "$A0_IMAGE" \ + bash -c 'set -e; mkdir -p /a0/usr; + printf "AUTH_LOGIN=%s\nAUTH_PASSWORD=%s\n" "$AUTH_LOGIN" "$AUTH_PASSWORD" > /a0/usr/.env; + printf "%s\n" "$A0_SEEDED_SETTINGS_JSON" > /a0/usr/settings.json; + exec /exe/initialize.sh "$BRANCH"' >/dev/null + +log " waiting up to ${BOOT_TIMEOUT}s for /login ..." +deadline=$(( SECONDS + BOOT_TIMEOUT )) +until curl -sf "$BASE/login" >/dev/null 2>&1; do + [ $SECONDS -ge $deadline ] && { podman logs --tail 50 "$A0_NAME" || true; fail "A0 did not become healthy in ${BOOT_TIMEOUT}s"; } + sleep 3 +done +log " ✓ A0 booted nested; /login is healthy" + +# --- 3. harness can authenticate ---------------------------------------------- +log "3/3 logging in as $AUTH_LOGIN" +curl -sf -c "$COOKIES" -b "$COOKIES" \ + --data-urlencode "username=$AUTH_LOGIN" \ + --data-urlencode "password=$AUTH_PASSWORD" \ + -o /dev/null "$BASE/login" || fail "login POST failed" + +# Authenticated fetch: unauthenticated requests to / redirect to /login; an +# authenticated session returns the app. -L follows; we assert we did NOT land +# back on the login form. +body="$(curl -s -L -b "$COOKIES" "$BASE/")" +echo "$body" | grep -qi 'name="password"' && fail "still seeing login form — auth did not stick" +log " ✓ authenticated session established" + +# Bonus (logged, not gating): try the plugins API with best-effort CSRF. +csrf="$(awk '/csrf_token/ {print $7}' "$COOKIES" | tail -1)" +if [ -n "${csrf:-}" ]; then + code="$(curl -s -o /dev/null -w '%{http_code}' -b "$COOKIES" \ + -H "X-CSRF-Token: $csrf" -H 'Content-Type: application/json' \ + --data '{"filter":{"custom":true}}' "$BASE/api/plugins_list" || true)" + log " (bonus) POST /api/plugins_list → HTTP $code" +fi + +log "PHASE-0 ACCEPTANCE PASSED — rootless nested A0 boot + auth confirmed." From b6c51baa13cf0b1ace7a86c30aa2ec3b43f52de4 Mon Sep 17 00:00:00 2001 From: omar-nahhas-agent Date: Fri, 19 Jun 2026 23:48:28 +0000 Subject: [PATCH 2/6] =?UTF-8?q?spike(devcontainer):=20base=20on=20quay.io/?= =?UTF-8?q?podman/stable=20=E2=80=94=20Ubuntu+apt-podman=20can't=20nest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spike finding: the Playwright/Ubuntu-jammy base + apt podman 3.4.4 fails rootless nesting ("cannot setup namespace using newuidmap"). podman/stable (podman 5.8.2, pre-configured for rootless nesting) nests cleanly — verified locally inside the built image (NESTED_IN_DEVKIT_OK, unprivileged). Playwright browser deps deferred to Phase 1 (Fedora install); the Phase-0 gate is nesting, which needs no browser. Ref: SPEC DEC-039. Co-Authored-By: Claude Opus 4.8 (1M context) --- devcontainer/Containerfile | 89 +++++++++++--------------------------- devcontainer/README.md | 13 ++++-- 2 files changed, 36 insertions(+), 66 deletions(-) diff --git a/devcontainer/Containerfile b/devcontainer/Containerfile index 2e495f3..e974572 100644 --- a/devcontainer/Containerfile +++ b/devcontainer/Containerfile @@ -1,72 +1,35 @@ # Agent Zero plugin devkit — unified execution environment. # -# One image, three uses (SPEC DEC-018): -# 1. GitHub Actions runs the plugin e2e lifecycle inside it. -# 2. A developer uses it as a dev container. -# 3. An AI agent runs it (docker OR podman) to develop + test locally. +# One image, three uses (SPEC DEC-018): CI runner, developer dev container, +# AI-agent local env. It boots the Agent Zero container *nested and +# unprivileged* via rootless podman — no --privileged, no host docker.sock. # -# It carries Playwright (browsers + deps) AND rootless podman, so it can boot -# the Agent Zero container *nested and unprivileged* — no --privileged, no host -# docker.sock (SPEC REQ-DEV-003 / REQ-CI-006). Proven mechanism: rootless -# podman-in-podman via `--device /dev/fuse --security-opt label=disable`. +# BASE CHOICE (Phase-0 spike finding, 2026-06-19): +# The obvious base — the official Playwright image (Ubuntu jammy) + apt +# `podman` — FAILS to nest rootlessly: jammy ships podman 3.4.4, and nested +# rootless `podman run` dies with "cannot setup namespace using newuidmap". +# quay.io/podman/stable (modern podman, purpose-built + pre-configured for +# rootless nesting) WORKS. So we base on it and add the harness tooling. # -# Base: the official Playwright image (Ubuntu jammy) already ships node + the -# browser binaries + their system libs — the painful half of the image. - -FROM mcr.microsoft.com/playwright:v1.48.0-jammy - -# --- rootless podman toolchain ------------------------------------------------- -# podman + the rootless plumbing: fuse-overlayfs (rootless storage), uidmap -# (newuidmap/newgidmap for subuid/subgid), slirp4netns (rootless networking). -RUN apt-get update \ - && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ - podman \ - fuse-overlayfs \ - uidmap \ - slirp4netns \ - crun \ - curl \ - jq \ - openssh-client \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# --- rootless configuration ---------------------------------------------------- -# The Playwright image ships an unprivileged user `pwuser` (uid 1000). Give it a -# subuid/subgid range so rootless podman can map a container's users. -RUN echo "pwuser:100000:65536" > /etc/subuid \ - && echo "pwuser:100000:65536" > /etc/subgid - -# Storage: overlay backed by fuse-overlayfs (works rootless, no kernel overlay -# mount privilege required). -RUN mkdir -p /etc/containers \ - && printf '%s\n' \ - '[storage]' \ - 'driver = "overlay"' \ - '[storage.options.overlay]' \ - 'mount_program = "/usr/bin/fuse-overlayfs"' \ - > /etc/containers/storage.conf \ - && printf '%s\n' \ - '[engine]' \ - 'cgroup_manager = "cgroupfs"' \ - 'events_logger = "file"' \ - '[containers]' \ - 'netns = "slirp4netns"' \ - > /etc/containers/containers.conf \ - && printf '%s\n' \ - 'unqualified-search-registries = ["docker.io"]' \ - > /etc/containers/registries.conf - -# A short, well-known registries policy so `podman pull docker.io/...` works. -RUN mkdir -p /home/pwuser/.config/containers \ - && chown -R pwuser:pwuser /home/pwuser/.config - -USER pwuser -ENV HOME=/home/pwuser +# Trade-off: it's Fedora-based, so Playwright browser system-deps are a +# Phase-1 addition (Playwright's `--with-deps` is Debian/Ubuntu-only; on +# Fedora the deps are installed explicitly). The Phase-0 GATE is *nesting*, +# which needs no browser — so the spike image carries only what the +# acceptance needs (podman + curl). Node/Playwright land in Phase 1. + +FROM quay.io/podman/stable + +# Harness tooling the Phase-0 acceptance needs. podman + fuse-overlayfs + +# the rootless plumbing already ship in this base and are pre-configured. +RUN microdnf install -y curl jq \ + && microdnf clean all + +# The base ships an unprivileged `podman` user with a working subuid/subgid +# range and newuidmap/newgidmap caps — the exact setup the Ubuntu path lacked. +USER podman WORKDIR /workspace -# Sanity: podman present and reports rootless. (Build-time check; nesting is -# exercised at run time by the spike.) +# Sanity: report the (modern) podman this base provides. RUN podman --version ENTRYPOINT ["/bin/bash"] diff --git a/devcontainer/README.md b/devcontainer/README.md index e3c4107..8bf2265 100644 --- a/devcontainer/README.md +++ b/devcontainer/README.md @@ -6,9 +6,16 @@ One image, three uses (SPEC `DEC-018`): 2. **Developers** use it as a dev container. 3. **AI agents** run it (docker or podman) to develop + test locally before pushing. -It carries **Playwright** (browsers + system libs, from the official Playwright base) -plus **rootless podman**, so it boots the Agent Zero container **nested and -unprivileged** — no `--privileged`, no host `docker.sock`. +It boots the Agent Zero container **nested and unprivileged** via **rootless podman** — +no `--privileged`, no host `docker.sock`. + +> **Spike finding (base image is load-bearing):** basing on the official Playwright image +> (Ubuntu jammy) + apt `podman` (3.4.4) **fails** to nest rootlessly — +> `cannot setup namespace using newuidmap`. Basing on `quay.io/podman/stable` +> (podman 5.8.2, purpose-built for nesting) **works** (verified locally: +> `NESTED_IN_DEVKIT_OK`). So the image bases on podman/stable; **Playwright** browsers are a +> **Phase-1** Fedora addition (the Phase-0 gate is *nesting*, which needs no browser). See +> SPEC `DEC-039`. ## Why this exists From cd3db4ec7274e9e7f272d8c36a679fcac0158033 Mon Sep 17 00:00:00 2001 From: omar-nahhas-agent Date: Fri, 19 Jun 2026 23:56:21 +0000 Subject: [PATCH 3/6] spike(devcontainer): fully-qualify A0 image (podman/stable short-name enforcing) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI proved the gate: on GH-hosted ubuntu-latest the devcontainer nests a rootless container (rootless:true, step 1/3 ✓). Step 2 failed only on "short-name resolution enforced but cannot prompt without a TTY" — podman/stable requires fully-qualified names. Pin docker.io/agent0ai/agent-zero:latest. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/devcontainer-spike.yml | 4 ++-- devcontainer/spike/run-acceptance.sh | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/devcontainer-spike.yml b/.github/workflows/devcontainer-spike.yml index 7827bac..13536a3 100644 --- a/.github/workflows/devcontainer-spike.yml +++ b/.github/workflows/devcontainer-spike.yml @@ -15,7 +15,7 @@ on: a0_image: description: "A0 image to boot nested (DEC-019 parameter)" required: false - default: "agent0ai/agent-zero:latest" + default: "docker.io/agent0ai/agent-zero:latest" push: branches: ["spike/devcontainer-phase0"] @@ -37,7 +37,7 @@ jobs: - name: Run Phase-0 acceptance INSIDE the devcontainer (nested, unprivileged) env: - A0_IMAGE: ${{ github.event.inputs.a0_image || 'agent0ai/agent-zero:latest' }} + A0_IMAGE: ${{ github.event.inputs.a0_image || 'docker.io/agent0ai/agent-zero:latest' }} run: | podman run --rm \ --device /dev/fuse \ diff --git a/devcontainer/spike/run-acceptance.sh b/devcontainer/spike/run-acceptance.sh index d320f52..b70de41 100755 --- a/devcontainer/spike/run-acceptance.sh +++ b/devcontainer/spike/run-acceptance.sh @@ -14,7 +14,8 @@ # Playwright harness (proven on livekit) — it is Phase-2, not this gate. set -euo pipefail -A0_IMAGE="${A0_IMAGE:-agent0ai/agent-zero:latest}" # DEC-019: parameterized +# Fully-qualified: podman/stable enforces short-name resolution (no TTY to prompt in CI). +A0_IMAGE="${A0_IMAGE:-docker.io/agent0ai/agent-zero:latest}" # DEC-019: parameterized A0_HTTP_PORT="${A0_HTTP_PORT:-8080}" A0_NAME="a0-spike" AUTH_LOGIN="${AUTH_LOGIN:-admin}" From 156e82a864fa513175e65702e7a34067a8009423 Mon Sep 17 00:00:00 2001 From: omar-nahhas-agent Date: Sat, 20 Jun 2026 00:06:02 +0000 Subject: [PATCH 4/6] spike(devcontainer): widen A0 boot window to 720s + better timeout diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI cd3db4e proved nesting works on GH ubuntu-latest AND A0 booted nested rootless (run_ui RUNNING, Flask serving) — but cold-CI first boot (pull + initialize.sh clone + pip install) reached services right as the 300s window expired. Bump BOOT_TIMEOUT→720s, job timeout→40m, and dump curl -v + podman logs on timeout for clearer diagnosis. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/devcontainer-spike.yml | 2 +- devcontainer/spike/run-acceptance.sh | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/devcontainer-spike.yml b/.github/workflows/devcontainer-spike.yml index 13536a3..7c47c58 100644 --- a/.github/workflows/devcontainer-spike.yml +++ b/.github/workflows/devcontainer-spike.yml @@ -22,7 +22,7 @@ on: jobs: rootless-nested-boot: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 40 steps: - uses: actions/checkout@v4 diff --git a/devcontainer/spike/run-acceptance.sh b/devcontainer/spike/run-acceptance.sh index b70de41..1da1361 100755 --- a/devcontainer/spike/run-acceptance.sh +++ b/devcontainer/spike/run-acceptance.sh @@ -20,7 +20,10 @@ A0_HTTP_PORT="${A0_HTTP_PORT:-8080}" A0_NAME="a0-spike" AUTH_LOGIN="${AUTH_LOGIN:-admin}" AUTH_PASSWORD="${AUTH_PASSWORD:-admin}" -BOOT_TIMEOUT="${BOOT_TIMEOUT:-300}" +# First boot in cold CI = image pull + initialize.sh (clone BRANCH + pip install), +# which comfortably exceeds 5 min on a fresh runner. CI run cd3db4e showed A0's +# services reaching RUNNING right as a 300s window expired — so give it room. +BOOT_TIMEOUT="${BOOT_TIMEOUT:-720}" BASE="http://localhost:${A0_HTTP_PORT}" COOKIES="$(mktemp)" @@ -55,7 +58,11 @@ podman run -d --name "$A0_NAME" \ log " waiting up to ${BOOT_TIMEOUT}s for /login ..." deadline=$(( SECONDS + BOOT_TIMEOUT )) until curl -sf "$BASE/login" >/dev/null 2>&1; do - [ $SECONDS -ge $deadline ] && { podman logs --tail 50 "$A0_NAME" || true; fail "A0 did not become healthy in ${BOOT_TIMEOUT}s"; } + if [ $SECONDS -ge $deadline ]; then + echo "--- curl -v (last attempt) ---"; curl -v "$BASE/login" 2>&1 | tail -15 || true + echo "--- podman logs (tail) ---"; podman logs --tail 80 "$A0_NAME" || true + fail "A0 did not become healthy in ${BOOT_TIMEOUT}s" + fi sleep 3 done log " ✓ A0 booted nested; /login is healthy" From 23ae917899af0f642bc42ce053501ef452a4a523 Mon Sep 17 00:00:00 2001 From: omar-nahhas-agent Date: Sat, 20 Jun 2026 00:24:47 +0000 Subject: [PATCH 5/6] =?UTF-8?q?spike(devcontainer):=20neutralize=20sshd=20?= =?UTF-8?q?=E2=80=94=20A0=20watchdog=20kills=20instance=20on=20its=20FATAL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run 156e82a: A0 booted nested rootless (run_ui RUNNING, Flask serving) but sshd exits 255 under rootless podman; A0's supervisor event-listener (no args) kills the whole instance on ANY process FATAL → A0 died ~8s after boot → /login connection-refused for the rest of the window. Replace /usr/sbin/sshd with a no-op before initialize.sh so run_sshd stays RUNNING and never FATALs. SSH isn't used by the HTTP e2e. Watchdog stays armed for genuinely-critical procs. Ref: SPEC DEC-040 / Appendix F.6. Co-Authored-By: Claude Opus 4.8 (1M context) --- devcontainer/spike/run-acceptance.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/devcontainer/spike/run-acceptance.sh b/devcontainer/spike/run-acceptance.sh index 1da1361..83b65e3 100755 --- a/devcontainer/spike/run-acceptance.sh +++ b/devcontainer/spike/run-acceptance.sh @@ -53,6 +53,13 @@ podman run -d --name "$A0_NAME" \ bash -c 'set -e; mkdir -p /a0/usr; printf "AUTH_LOGIN=%s\nAUTH_PASSWORD=%s\n" "$AUTH_LOGIN" "$AUTH_PASSWORD" > /a0/usr/.env; printf "%s\n" "$A0_SEEDED_SETTINGS_JSON" > /a0/usr/settings.json; + # Rootless-nesting workaround: /usr/sbin/sshd exits 255 under rootless + # podman (privsep needs caps a mapped-root lacks). A0s supervisor + # event-listener kills the WHOLE instance on any process FATAL, so a + # flapping sshd takes A0 down ~8s after boot. SSH is irrelevant to the + # HTTP e2e, so replace sshd with a no-op that never exits → run_sshd + # stays RUNNING, watchdog stays armed for genuinely-critical procs. + printf "#!/bin/sh\nexec tail -f /dev/null\n" > /usr/sbin/sshd; chmod +x /usr/sbin/sshd; exec /exe/initialize.sh "$BRANCH"' >/dev/null log " waiting up to ${BOOT_TIMEOUT}s for /login ..." From ee1a4fb8a8ba4e48f67b7bf1ab4fb2750bdfbc8b Mon Sep 17 00:00:00 2001 From: omar-nahhas-agent Date: Sat, 20 Jun 2026 00:44:08 +0000 Subject: [PATCH 6/6] spike(devcontainer): bind port 80 nested via outer-sysctl + inner host-net MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run 23ae917: A0 stayed up (sshd fix worked) but its UI couldn't bind 0.0.0.0:80 — privileged port, rootless mapped-root denied. Inner container defaults to host netns so a --sysctl on the inner run is rejected. Verified-working model (reproduced locally, REACHED_80_OK): outer devcontainer run with --sysctl net.ipv4.ip_unprivileged_port_start=0, inner A0 with --network=host, reach at localhost:80. Ref: SPEC DEC-041. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/devcontainer-spike.yml | 1 + devcontainer/spike/run-acceptance.sh | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/devcontainer-spike.yml b/.github/workflows/devcontainer-spike.yml index 7c47c58..24225dc 100644 --- a/.github/workflows/devcontainer-spike.yml +++ b/.github/workflows/devcontainer-spike.yml @@ -42,6 +42,7 @@ jobs: podman run --rm \ --device /dev/fuse \ --security-opt label=disable \ + --sysctl net.ipv4.ip_unprivileged_port_start=0 \ -e A0_IMAGE="$A0_IMAGE" \ -v "$PWD":/workspace:Z \ -w /workspace \ diff --git a/devcontainer/spike/run-acceptance.sh b/devcontainer/spike/run-acceptance.sh index 83b65e3..465551e 100755 --- a/devcontainer/spike/run-acceptance.sh +++ b/devcontainer/spike/run-acceptance.sh @@ -16,7 +16,11 @@ set -euo pipefail # Fully-qualified: podman/stable enforces short-name resolution (no TTY to prompt in CI). A0_IMAGE="${A0_IMAGE:-docker.io/agent0ai/agent-zero:latest}" # DEC-019: parameterized -A0_HTTP_PORT="${A0_HTTP_PORT:-8080}" +# A0 serves on port 80. Under nested rootless, the inner A0 container shares the +# devcontainer's network namespace (--network=host); binding 80 there is allowed +# because the WORKFLOW runs the devcontainer with +# --sysctl net.ipv4.ip_unprivileged_port_start=0. So we reach A0 at localhost:80. +A0_HTTP_PORT="${A0_HTTP_PORT:-80}" A0_NAME="a0-spike" AUTH_LOGIN="${AUTH_LOGIN:-admin}" AUTH_PASSWORD="${AUTH_PASSWORD:-admin}" @@ -45,7 +49,7 @@ log " ✓ nested rootless container ran" # --- 2. boot the real A0 image nested ----------------------------------------- log "2/3 booting A0 nested: $A0_IMAGE on :$A0_HTTP_PORT" podman run -d --name "$A0_NAME" \ - -p "${A0_HTTP_PORT}:80" \ + --network=host \ -e AUTH_LOGIN="$AUTH_LOGIN" -e AUTH_PASSWORD="$AUTH_PASSWORD" \ -e A0_SEEDED_SETTINGS_JSON='{}' \ -e BRANCH=main \