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
18 changes: 10 additions & 8 deletions .github/workflows/nightly-qemu.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
name: nightly-qemu
# Phase 6 Plan 06-03 Task 3. Nightly QEMU release-gate harness (TST-03).
# Replaces the Phase 1 empty-guard scaffold with a real matrix run against
# both supported Ubuntu LTS images. Docker (test.yml) runs every PR; this
# every supported Ubuntu LTS image. Docker (test.yml) runs every PR; this
# workflow runs nightly + workflow_dispatch and is the release-gate-adjacent
# "Docker-alone is disqualified" (ADR-007) signal.
# "Docker-alone is disqualified" (ADR-007) signal. AGE-11 added 26.04
# ("Resolute Raccoon") on 2026-04-26.

on:
schedule:
- cron: '0 3 * * *' # 03:00 UTC daily
workflow_dispatch:
inputs:
ubuntu:
description: 'Ubuntu version(s) to run (22.04 / 24.04 / both)'
description: 'Ubuntu version(s) to run (22.04 / 24.04 / 26.04 / all)'
required: false
default: both
default: all
type: choice
options:
- both
- all
- '22.04'
- '24.04'
- '26.04'

permissions:
contents: read
Expand All @@ -31,17 +33,17 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu: ['22.04', '24.04']
ubuntu: ['22.04', '24.04', '26.04']
steps:
- uses: actions/checkout@v5

# Skip the matrix leg when workflow_dispatch targeted a single version.
# Cron runs (github.event_name == 'schedule') always exercise both.
# Cron runs (github.event_name == 'schedule') always exercise all arms.
- name: Decide whether to run this matrix leg
id: decide
run: |
dispatched='${{ github.event.inputs.ubuntu }}'
if [[ -z "$dispatched" || "$dispatched" == 'both' ]]; then
if [[ -z "$dispatched" || "$dispatched" == 'all' || "$dispatched" == 'both' ]]; then
echo "run=true" >> "$GITHUB_OUTPUT"
elif [[ "$dispatched" == '${{ matrix.ubuntu }}' ]]; then
echo "run=true" >> "$GITHUB_OUTPUT"
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,20 @@ jobs:
shell: bash
run: pnpm exec node --test dist-test/test/*.test.js

# Gate 2 — Docker matrix. Runs tests/docker/run.sh on both Ubuntu LTS
# images. The Docker harness executes the full tests/bats/ suite INCLUDING
# Gate 2 — Docker matrix. Runs tests/docker/run.sh on every supported Ubuntu
# LTS image. The Docker harness executes the full tests/bats/ suite INCLUDING
# tests/bats/51-agt02-release-gate.bats — this satisfies TST-05's
# "AGT-02 inside Docker" half. fail-fast:false so a 22.04 red still shows
# the 24.04 signal (and vice versa) for faster triage.
# "AGT-02 inside Docker" half. fail-fast:false so any arm failing still
# shows the others' signal for faster triage. AGE-11 added 26.04 on
# 2026-04-26.
gate-2-docker:
needs: gate-1-precommit
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
ubuntu: [ubuntu-22.04, ubuntu-24.04]
ubuntu: [ubuntu-22.04, ubuntu-24.04, ubuntu-26.04]
steps:
- uses: actions/checkout@v5
- name: Docker bats matrix (incl. 51-*.bats — TST-05 inside Docker)
Expand All @@ -147,7 +148,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ubuntu: ['22.04', '24.04']
ubuntu: ['22.04', '24.04', '26.04']
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,16 @@ jobs:
# systemctl is-system-running wait is the usual long-tail; this caps it.
timeout-minutes: 15
strategy:
# Phase 2 acceptance matrix: BOTH supported Ubuntu LTS versions run on
# every PR. fail-fast=false so 22.04 failure still reports 24.04 (and
# vice versa) — Phase 6 release gate needs to see both arms.
# Phase 2 acceptance matrix: every supported Ubuntu LTS version runs on
# every PR. fail-fast=false so a single arm failure still reports the
# others — Phase 6 release gate needs to see all arms. AGE-11 added
# 26.04 ("Resolute Raccoon") on 2026-04-26.
fail-fast: false
matrix:
ubuntu:
- ubuntu-22.04
- ubuntu-24.04
- ubuntu-26.04
steps:
- uses: actions/checkout@v5
# Empty-plugin guard from Phase 1: still useful. After Phase 2 lands the
Expand Down
2 changes: 1 addition & 1 deletion .planning/PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Known minor issue: OG image (SVG format) doesn't render on all social platforms

## Constraints

- **Target OS (v0.3.0):** Ubuntu (LTS). Fedora/CentOS/Alma/Arch deferred.
- **Target OS (v0.3.0):** Ubuntu LTS — 22.04 (Jammy), 24.04 (Noble), 26.04 (Resolute Raccoon, added 2026-04-26 per AGE-11). Fedora/CentOS/Alma/Arch deferred.
- **Install UX:** one-command from user perspective; internally may chain apt / curl / npm / fpm.
- **Node.js ownership:** must be owned by the agent user or use a writable global prefix under the agent user's home — no `sudo npm install -g`.
- **Test harness:** containerized or QEMU-based; no real-hardware / cloud-VM deploy step.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Pivoted from custom distro (v0.2.0) on 2026-04-18. See
steps, catalog, registry CLI in `plugin/cli/`)
- `tests/bats/` — behavior-contract suite (BHV-XX / RT-XX / AGT-XX / CLI-XX / CAT-XX / INST-XX)
- `tests/harness/` — harness meta-tests (Phase 1 acceptance gate)
- `tests/docker/` — fast CI harness (Ubuntu 22.04 + 24.04 matrix, every PR)
- `tests/docker/` — fast CI harness (Ubuntu 22.04 + 24.04 + 26.04 matrix, every PR)
- `tests/qemu/` — release-gate harness (fresh cloud images, nightly + release)
- `packaging/` — curl-pipe-bash installer + optional fpm .deb wrapper
- `docs/` — reference documentation (`HARNESS.md`, `decisions/`, `research/`, `proposals/`, `reviews/`)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ sudo fights. Curated stable versions; explicit override with `agentlinux pin`.

## Install

One command on a clean Ubuntu 22.04 or 24.04 host (root/sudo required):
One command on a clean Ubuntu 22.04, 24.04 or 26.04 host (root/sudo required):

```bash
curl -fsSL https://agentlinux.org/install.sh | sudo bash
Expand Down Expand Up @@ -67,8 +67,8 @@ and the `nodejs` package as well. A re-run of the curl installer after
## Stability model

AgentLinux ships *curated combos*: every catalog agent is pinned to an exact
version that we test together end-to-end (Docker × {22.04, 24.04} + QEMU ×
{22.04, 24.04}) before each release. When you install an agent, you get the
version that we test together end-to-end (Docker × {22.04, 24.04, 26.04} +
QEMU × {22.04, 24.04, 26.04}) before each release. When you install an agent, you get the
curated pin; when you want to run ahead of it, you can — `agentlinux upgrade`
shows the 3-way divergence between installed, curated, and upstream latest,
and `agentlinux pin` sets sticky overrides so power users are not re-nagged.
Expand All @@ -95,9 +95,9 @@ catalog's curated choice. Precedent: Homebrew's `brew pin`.

## Requirements

- Ubuntu 22.04 LTS or 24.04 LTS (x86_64)
- Ubuntu 22.04 LTS, 24.04 LTS, or 26.04 LTS (x86_64)
- root or sudo access for the one-time install
- `curl` preinstalled (stock on both releases)
- `curl` preinstalled (stock on all three releases)

Not yet supported in v0.3.0: ARM64, Fedora/Alma/Rocky/Arch. Those are on the
v0.4+ roadmap. See [.planning/REQUIREMENTS.md](.planning/REQUIREMENTS.md) for
Expand Down
3 changes: 2 additions & 1 deletion docs/HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ agent-linux/ # Workspace root
│ ├── docker/ # Fast CI harness — Dockerfile per Ubuntu version
│ │ ├── Dockerfile.ubuntu-22.04
│ │ ├── Dockerfile.ubuntu-24.04
│ │ ├── Dockerfile.ubuntu-26.04
│ │ └── run.sh # Orchestrates: build image → run installer → run bats
│ └── qemu/ # Definitive release-gate harness — cloud-image VMs
│ ├── boot.sh # Fresh Ubuntu cloud image → SSH → install → bats
Expand Down Expand Up @@ -282,7 +283,7 @@ External systems that agents interact with during AgentLinux development. Compar
| Playwright (browser-access tool for agents) | `npm view playwright` + Playwright docs | Full | — |
| GSD npm package | Local GSD install + `npm view get-shit-done-cc` | Full | — |
| Ubuntu cloud images (QEMU test harness) | Cloud-images.ubuntu.com download + QEMU local | Partial | **P1:** cache downloaded images, boot helper skill |
| Docker Hub (ubuntu:22.04, ubuntu:24.04) | `docker pull` via GH Actions | Full | — |
| Docker Hub (ubuntu:22.04, ubuntu:24.04, ubuntu:26.04) | `docker pull` via GH Actions | Full | — |
| agentlinux.org (website + releases host) | GitHub Pages deploy via Actions | Full | — |
| Context7 MCP (library docs lookup) | Configured via `.mcp.json` | Full | — |

Expand Down
9 changes: 6 additions & 3 deletions packaging/curl-installer/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ check_root() {
}

# ------------------------------------------------------------------------------
# Parse /etc/os-release; die unless it declares Ubuntu 22.04 or 24.04.
# Parse /etc/os-release; die unless it declares Ubuntu 22.04, 24.04 or 26.04.
# Source: 06-RESEARCH.md lines 920-936 (Example: Ubuntu version detection).
# Keep this allowlist in lockstep with plugin/lib/distro_detect.sh — both gate
# the same support matrix and the curl-installer test fixture exercises this
# path before handing off to the staged installer.
# ------------------------------------------------------------------------------
detect_ubuntu_version() {
local id version
Expand All @@ -81,9 +84,9 @@ detect_ubuntu_version() {
[[ "$id" == "ubuntu" ]] \
|| die "unsupported distro: ${id} (AgentLinux v0.3.0 supports Ubuntu only)"
case "$version" in
22.04 | 24.04) ;;
22.04 | 24.04 | 26.04) ;;
*)
die "unsupported Ubuntu version: ${version} (AgentLinux v0.3.0 supports 22.04 and 24.04 only)"
die "unsupported Ubuntu version: ${version} (AgentLinux v0.3.0 supports 22.04, 24.04 and 26.04 only)"
;;
esac
}
Expand Down
2 changes: 1 addition & 1 deletion plugin/bin/agentlinux-install
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Options:
users may depend on Node.js, so this is NOT the
default purge behavior.

The installer must run as root. Supported hosts: Ubuntu 22.04 and 24.04.
The installer must run as root. Supported hosts: Ubuntu 22.04, 24.04 and 26.04.
Transcript: ${LOG_FILE}
EOF
}
Expand Down
14 changes: 7 additions & 7 deletions plugin/lib/distro_detect.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# plugin/lib/distro_detect.sh — Ubuntu 22.04 / 24.04 detection.
# plugin/lib/distro_detect.sh — Ubuntu 22.04 / 24.04 / 26.04 detection.
#
# The installer refuses to run on anything other than Ubuntu 22.04 or 24.04
# future distros land in v0.4+ per ADR. `detect_distro` exports
# The installer refuses to run on anything other than Ubuntu 22.04, 24.04 or
# 26.04 — future distros land in v0.4+ per ADR. `detect_distro` exports
# AGENTLINUX_DISTRO_VERSION for downstream provisioners that need to branch
# (e.g. locale-gen no-op on C.UTF-8, Pitfall 5 in 02-RESEARCH.md).
#
Expand All @@ -24,8 +24,8 @@ fi
#
# Escape hatch: AGENTLINUX_SKIP_DISTRO_CHECK=1 bypasses validation and exports
# AGENTLINUX_DISTRO_VERSION=unchecked. Intended ONLY for bats unit sourcing on
# dev hosts that are not themselves Ubuntu 22.04/24.04. Real installer runs
# MUST NOT set this.
# dev hosts that are not themselves Ubuntu 22.04/24.04/26.04. Real installer
# runs MUST NOT set this.
detect_distro() {
if [[ "${AGENTLINUX_SKIP_DISTRO_CHECK:-0}" == "1" ]]; then
export AGENTLINUX_DISTRO_VERSION="unchecked"
Expand All @@ -49,12 +49,12 @@ detect_distro() {
fi

case "${VERSION_ID:-}" in
22.04 | 24.04)
22.04 | 24.04 | 26.04)
export AGENTLINUX_DISTRO_VERSION="$VERSION_ID"
log_info "detected ubuntu ${VERSION_ID}"
;;
*)
log_error "unsupported ubuntu version: ${VERSION_ID:-unset} (required: 22.04 or 24.04)"
log_error "unsupported ubuntu version: ${VERSION_ID:-unset} (required: 22.04, 24.04 or 26.04)"
return 1
;;
esac
Expand Down
44 changes: 44 additions & 0 deletions plugin/lib/idempotency.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,50 @@ if ! command -v log_error >/dev/null 2>&1; then
return 1 2>/dev/null || exit 1
fi

# write_file_atomic <mode> <dest>
# Atomic full-file overwrite from stdin: write the heredoc/pipe body to a
# tmpfile in the same directory as <dest>, then install(1) it into place.
# Same semantics as `install -m <mode> /dev/stdin <dest>` on GNU coreutils,
# but portable to uutils-coreutils 0.7.0 (the Rust rewrite shipped on Ubuntu
# 26.04). uutils' install recursively readlink-chases /dev/stdin →
# /proc/self/fd/0 → "pipe:[NNN]" and then tries to stat the synthetic pipe
# name as a path, ENOENTing with "install: No such file or directory" — but
# only when the destination ALREADY exists (first run succeeds, idempotent
# re-runs fail). Symptom diagnosed via strace on Ubuntu 26.04 during the
# AGE-11 supported-target rollout (see PR #5 / commit fixing INST-02 + BHV-07
# byte-stability on uutils). Keep this helper in the lib so future calls
# cannot regress to /dev/stdin under set -e.
#
# Same-directory tmpfile placement keeps the install(1) rename atomic — a
# cross-filesystem rename would fall back to copy+unlink and lose atomicity.
# The tmpfile is hidden (leading dot) and unlinked unconditionally; on install
# failure the function returns non-zero so the caller's set -euo pipefail trap
# fires and the operator sees the underlying install diagnostic in the log.
write_file_atomic() {
if [[ $# -lt 2 ]]; then
log_error "write_file_atomic: missing arguments (usage: write_file_atomic <mode> <dest>)"
return 64
fi
local mode=$1 dest=$2
local dir base tmp rc=0
dir=$(dirname -- "$dest")
base=$(basename -- "$dest")
tmp=$(mktemp -p "$dir" ".${base}.XXXXXX")
# shellcheck disable=SC2064
# Expand $tmp at trap-install time (function-local var); resolving later
# would re-read a stale binding if the variable were reassigned.
# Symmetric with ensure_marker_block's RETURN trap — guarantees tmpfile
# cleanup on any exit path, including a `cat` that aborts mid-write under
# set -e (ENOSPC, SIGPIPE).
trap "rm -f -- '$tmp'" RETURN
cat >"$tmp"
install -m "$mode" "$tmp" "$dest" || rc=$?
if [[ $rc -ne 0 ]]; then
log_error "write_file_atomic: install -m ${mode} failed for ${dest} (rc=${rc})"
return "$rc"
fi
}

# ensure_line_in_file <line> <file>
# Append <line> to <file> only if not already present (fixed-string
# whole-line match). `-F` = literal string, `-x` = whole-line, `-q` = quiet,
Expand Down
27 changes: 17 additions & 10 deletions plugin/provisioner/40-path-wiring.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@
#
# Invariants enforced by this file (the plan's acceptance greps cross-verify
# the forbidden substrings literally do not appear anywhere in this source):
# - State mutation routes through `install -m 0644 /dev/stdin <<EOF`
# (installer-owned full-file writes) or `ensure_marker_block` (partial
# edit on user-owned ~agent/.bashrc). No raw-append or in-place editing.
# - State mutation routes through `write_file_atomic <mode> <dest> <<EOF`
# (installer-owned full-file writes — same atomic-rename semantics as
# `install -m <mode> /dev/stdin <dest>`, but portable to uutils-coreutils
# on Ubuntu 26.04) or `ensure_marker_block` (partial edit on user-owned
# ~agent/.bashrc). No raw-append or in-place editing.
# - Zero privilege-escalation configuration shipped: Phase 2 locks the
# no-default-drop-in rule; future phases use visudo_validate + 0440.
# - Zero wrapper shim pointing at an agent-owned binary from a root-owned
Expand All @@ -59,12 +61,17 @@ ensure_dir /home/agent/.local/bin 0755 agent:agent
# Sourced by /etc/profile on login shells. Re-source guard prevents double
# PATH-prepend if the file is sourced twice in the same shell session
# (T-02-09 mitigation + INST-02 idempotency contract).
# `install -m 0644 /dev/stdin` is an atomic full-file overwrite: re-runs of
# this provisioner produce byte-identical content (no env-variable
# substitution by the heredoc body since we use the 'PROFILE' single-quoted
# tag — `$PATH` etc. stay literal in the written file).
# `write_file_atomic` is the portable analogue of
# `install -m 0644 /dev/stdin <dest>` — same atomic-rename semantics, but
# routed through a same-directory tmpfile so it works on uutils-coreutils 0.7.0
# (the Rust `install` shipped on Ubuntu 26.04). uutils' install recursively
# readlink-chases /dev/stdin → /proc/self/fd/0 → "pipe:[NNN]" and ENOENTs
# whenever the destination already exists; the first run succeeds, the
# idempotent re-run fails. See plugin/lib/idempotency.sh `write_file_atomic`
# header for the full strace trail. The single-quoted heredoc tag keeps `$PATH`
# etc. literal in the written file.
# ---------------------------------------------------------------
install -m 0644 /dev/stdin /etc/profile.d/agentlinux.sh <<'PROFILE'
write_file_atomic 0644 /etc/profile.d/agentlinux.sh <<'PROFILE'
# AgentLinux login environment (generated by agentlinux-install).
# Sourced by /etc/profile on interactive login shells AND `sudo -u agent -i`.
# Re-source guard: bails on second source in the same shell session so that
Expand Down Expand Up @@ -143,7 +150,7 @@ log_info "wrote agentlinux-path marker block to /home/agent/.bashrc (--top)"
# split-brain avoidance; a future accidental divergence fails the plan's
# cross-grep acceptance criterion.
# ---------------------------------------------------------------
install -m 0644 /dev/stdin /etc/agentlinux.env <<'ENVFILE'
write_file_atomic 0644 /etc/agentlinux.env <<'ENVFILE'
PATH=/home/agent/.npm-global/bin:/home/agent/.local/bin:/usr/local/bin:/usr/bin:/bin
NPM_CONFIG_PREFIX=/home/agent/.npm-global
LANG=C.UTF-8
Expand All @@ -160,7 +167,7 @@ log_info "wrote /etc/agentlinux.env (systemd EnvironmentFile + cron header templ
# this PATH byte-identical to /etc/agentlinux.env — the plan's acceptance
# criterion cross-greps both for the same literal string.
# ---------------------------------------------------------------
install -m 0644 /dev/stdin /etc/cron.d/agentlinux <<'CRON'
write_file_atomic 0644 /etc/cron.d/agentlinux <<'CRON'
# AgentLinux cron environment (generated by agentlinux-install).
# Any agent cron job placed in this file inherits the PATH/locale below.
# Phase 2 ships NO default jobs. Example shape (do NOT uncomment):
Expand Down
Loading
Loading