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
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// Dev Container for the Code Factory worker image.
//
// Same Dockerfile target as compose and CI: an isolated non-root container with
// the checksum-pinned agent/development toolchain already converged by
// the latest, checksum-verified agent/development toolchain (the three omp
// marketplace plugins are the one unverified exception) already converged by
// `./factory apply --config containers/factory.container.yml`.
//
// Not provided, by design: systemd, user-manager linger, SSH server, Tailscale
Expand Down
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Keeps every SHA-pinned GitHub Action at the commit of its latest release.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
44 changes: 15 additions & 29 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
# behavior smoke in tests/container-smoke.sh.
#
# Supply chain: least-privilege token, no write permissions anywhere, and every
# third-party action pinned to an immutable commit SHA (verified 2026-09-15 via
# api.github.com):
# actions/checkout v7.0.1 -> 3d3c42e5aac5ba805825da76410c181273ba90b1
# astral-sh/setup-uv v10.1.0 -> bec219d24cd3e171d82865faccec33120bb574f4 (immutable release)
# uv itself is not pinned here: the version is read from toolchain.lock.json
# (tools.uv.version), the same pin ./bootstrap.sh installs, so the CI runner and
# the image can never drift apart.
# third-party action pinned to the commit SHA of its latest release, with the
# tag in a trailing comment (verified 2026-10-01 via the GitHub REST API).
# .github/dependabot.yml moves the pins to each new release weekly.
# uv and Bun are not pinned: like every ./factory apply, CI installs their
# latest releases.
name: ci

on:
Expand Down Expand Up @@ -46,33 +44,20 @@ jobs:
with:
persist-credentials: false

- name: Resolve the uv version pinned by the toolchain lock
id: uv
run: |
set -euo pipefail
version=$(jq -er '.tools.uv.version' toolchain.lock.json)
printf 'version=%s\n' "${version}" >> "$GITHUB_OUTPUT"

- name: Install pinned uv
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
- name: Install latest uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: ${{ steps.uv.outputs.version }}
version: latest
enable-cache: true

- name: Sync locked dependencies
run: uv sync --locked --group dev

- name: Resolve the Bun version pinned by the toolchain lock
id: bun
run: |
set -euo pipefail
version=$(jq -er '.tools.bun.version' toolchain.lock.json)
printf 'version=%s\n' "${version}" >> "$GITHUB_OUTPUT"

- name: Install pinned Bun
# Bun tracks its latest release, the same as every ./factory apply.
- name: Install latest Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ steps.bun.outputs.version }}
bun-version: latest

- name: Lint
run: uv run ruff check .
Expand Down Expand Up @@ -104,7 +89,7 @@ jobs:
- name: Smoke script syntax
run: bash -n tests/container-smoke.sh

- name: Audit image pins in the Dockerfile
- name: Audit image bases in the Dockerfile
run: |
set -euo pipefail
python3 - Dockerfile <<'PY'
Expand All @@ -123,8 +108,9 @@ jobs:
if not match:
continue
ref, alias = expand(match.group(1)), match.group(2)
if ref not in stages and "@sha256:" not in ref:
problems.append(f"FROM {ref} is neither a previous stage nor digest-pinned")
# The base tracks the newest Ubuntu LTS; nothing else is pulled.
if ref not in stages and ref != "ubuntu:latest":
problems.append(f"FROM {ref} is neither a previous stage nor ubuntu:latest")
if alias:
stages.add(alias)

Expand Down
9 changes: 4 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ Code Factory is an Ansible playbook with a Python CLI wrapper (`scripts/factory.
- `ansible/tasks/*.yml` — the tasks themselves (one file per concern)
- `ansible/templates/*.j2` — systemd unit templates
- `scripts/factory.py` — CLI (`init`, `validate`, `plan`, `apply`, `doctor`)
- `scripts/install_tools.py` — pinned binary installer (idempotent)
- `toolchain.lock.json` — every binary, every version, every sha256
- `scripts/install_tools.py` — latest-release tool installer (idempotent)
- `fleet/` — runtime scripts deployed to `~/oss-fleet/` on the target host
- `config/` — per-tool config templates deployed to Firstmate homes

Expand All @@ -21,7 +20,7 @@ Every task is idempotent; a second unchanged `apply` reports `changed=0`.

Every task is check-mode safe: `plan` (Ansible `--check`) previews without mutating.

Every binary is sha256-pinned in `toolchain.lock.json`. No floating `@latest` tags. Exceptions by design: herdr and omp track their latest release, resolved once per apply (herdr is verified against the SHA-256 its release publishes), and `verify.yml` asserts the resolved version is the installed one.
Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts; what each source is verified against, and the omp marketplace plugin exception, are in [Dependencies](docs/dependencies.md). Only the repository's own Python environment (`uv.lock`) stays locked, and CI pins each GitHub Action to the commit SHA of its latest release, kept current by Dependabot.

No unconditional restarts, daemon-reloads, or bare commands.

Expand All @@ -48,8 +47,8 @@ uv run ansible-playbook -i ansible/inventory.yml ansible/site.yml --syntax-check

## Adding a new tool

1. Add the release asset to `toolchain.lock.json` with the correct `format` (`file` or `tar`), sha256, URL, and `binaries` map.
2. Add the tool name to `factory_core_tools` in `group_vars/all.yml` (or a profile-gated list).
1. Add it to `GITHUB_LATEST` (a GitHub release whose assets carry a SHA-256 digest) or `NPM_LATEST` in `scripts/install_tools.py`. A tool from elsewhere needs its own resolver in `resolve_latest` that reads the checksum its publisher posts for the release.
2. Register where it installs: `factory_core_tools` in `group_vars/all.yml` for every host, `AGENT_TOOLS` in `scripts/install_tools.py` for the `agents` profile's native tools (npm tools in `NPM_LATEST` need no step), or `factory_installer_also` in `group_vars/all.yml` for a source Ansible installs itself.
3. If it needs a systemd unit, add a `.j2` template in `ansible/templates/` and wire it in the relevant task file.
4. If it needs environment variables, add them to `group_vars/all.yml` (not to shell rc files).
5. Update `docs/architecture.md` if the tool changes the host's architecture.
Expand Down
46 changes: 21 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,8 @@
# start_services=false and disables the docker/tailscale/desktop profiles, so the
# playbook performs file/tool convergence only.
#
# Base image pinned by digest, verified 2026-09-15 against registry-1.docker.io:
# ubuntu:24.04
# index digest sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254
# annotations org.opencontainers.image.version=24.04
# org.opencontainers.image.created=2026-09-05T00:00:00Z
# org.opencontainers.image.source=https://git.launchpad.net/cloud-images/+oci/ubuntu-base
# linux/amd64 sha256:a61567bd31828687156d735ea8eb01ba4e37636e225dd6a48ba94136a70d9d61
# linux/arm64 sha256:ec0b1c9058e44c837a21c3f9d8a3d5e9aaa94ed28edceb18e154af5efecf0950
# Base image: ubuntu:latest, the newest Ubuntu LTS. Nothing is pinned; the
# registry's content digests verify the layers that are pulled.
#
# Build targets:
# base OS packages and the factory account only (no repository content)
Expand All @@ -26,13 +20,13 @@
#
# Distribution packages are intentionally not version-frozen: docs/security.md
# treats operating-system security updates as an OS responsibility rather than
# pinning a whole vulnerable package index. Reproducibility comes from the base
# image digest plus the checksum-pinned tool lock consumed by the installer.
# pinning a whole vulnerable package index. Every tool the installer adds is
# its latest release, verified against the checksum its publisher posts (the
# three omp marketplace plugins are the one exception: no publisher posts one).

ARG UBUNTU_IMAGE=ubuntu:24.04
ARG UBUNTU_DIGEST=sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254
ARG UBUNTU_IMAGE=ubuntu:latest

FROM ${UBUNTU_IMAGE}@${UBUNTU_DIGEST} AS base
FROM ${UBUNTU_IMAGE} AS base

ARG DEBIAN_FRONTEND=noninteractive
ARG FACTORY_USER=coder
Expand All @@ -44,7 +38,7 @@ ARG FACTORY_WORKSPACE=/home/coder/Dev
# Prerequisites for: the uv bootstrap, ansible-core running against localhost
# (ansible.builtin.apt imports python3-apt from the system interpreter, so it is
# installed here instead of being auto-installed mid-playbook), the
# checksum-pinned tool installer (curl/ca-certificates/unzip/xz), the
# checksum-verifying tool installer (curl/ca-certificates/unzip/xz), the
# development profile (rustup toolchains need a C toolchain and pkg-config),
# and the behavior smoke script (procps/iproute2/jq).
RUN set -eux; \
Expand Down Expand Up @@ -144,8 +138,10 @@ WORKDIR /opt/code-factory
# smoke script are invoked directly, including from a context that lost them.
RUN set -eux; chmod +x bootstrap.sh factory tests/container-smoke.sh

# Pinned uv bootstrap + locked Python dependencies (no provisioning yet).
RUN set -eux; ./bootstrap.sh
# Latest uv bootstrap + locked Python dependencies (no provisioning yet). The
# one uv lookup takes the same optional `github_token` secret as `apply`.
RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \
set -eux; ./bootstrap.sh

# Schema validation through the repository's own validator.
RUN set -eux; ./factory validate --config "${FACTORY_CONFIG}"
Expand All @@ -155,13 +151,14 @@ RUN set -eux; ./factory validate --config "${FACTORY_CONFIG}"
# ordinary container cannot host.
RUN set -eux; uv run --project . --locked python containers/assert-image-config.py "${FACTORY_CONFIG}"

# The real convergence run. `apply` installs the checksum-pinned agent and
# development toolchain through scripts/install_tools.py and renders the
# user-scope files; start_services=false keeps it off systemd and linger.
# The optional `github_token` BuildKit secret authenticates the latest-herdr
# lookup (shared CI runner IPs exhaust the unauthenticated API budget). It is
# exposed to this step only, never as an ARG, ENV, layer file or history entry;
# without it the lookup runs unauthenticated.
# The real convergence run. `apply` installs the latest, checksum-verified
# agent and development toolchain (the three omp marketplace plugins are the one
# unverified exception) through scripts/install_tools.py and renders
# the user-scope files; start_services=false keeps it off systemd and linger.
# The optional `github_token` BuildKit secret authenticates the latest-release
# lookups (shared CI runner IPs exhaust the unauthenticated API budget). It is
# exposed to the lookup steps only (bootstrap and this one), never as an ARG,
# ENV, layer file or history entry; without it the lookup runs unauthenticated.
RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \
set -eux; ./factory apply --config "${FACTORY_CONFIG}"

Expand All @@ -171,8 +168,7 @@ ENV CODE_FACTORY_IMAGE=worker \
LABEL org.opencontainers.image.title="code-factory-worker" \
org.opencontainers.image.description="Isolated non-root Code Factory worker; no systemd, Tailscale or desktop." \
org.opencontainers.image.source="https://github.com/undeemed/Code-Factory" \
org.opencontainers.image.base.name="docker.io/library/ubuntu:24.04" \
org.opencontainers.image.base.digest="sha256:224a1869083a311ef3f13648a154ba79832fbef6364d31493642ca03082da254"
org.opencontainers.image.base.name="docker.io/library/ubuntu:latest"

WORKDIR ${FACTORY_WORKSPACE}
CMD ["/bin/bash"]
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# ⚡ Code Factory

Turn a fresh Ubuntu machine into a reproducible AI-agent coding host: Herdr, Firstmate, and the omp agent fleet, provisioned by Ansible from a pinned, checksum-locked toolchain. No Nix, no chezmoi, no cloud dependencies.
Turn a fresh Ubuntu machine into a reproducible AI-agent coding host: Herdr, Firstmate, and the omp agent fleet, provisioned by Ansible from a checksum-verified toolchain (the three omp marketplace plugins are the one exception). No Nix, no chezmoi, no cloud dependencies.

```mermaid
flowchart TD
box["Fresh Ubuntu 24.04 or 26.04"] --> boot["./bootstrap.sh: pinned uv, Ansible"]
box["Fresh Ubuntu 24.04 or 26.04"] --> boot["./bootstrap.sh: latest uv, Ansible"]
boot --> init["./factory init: writes .local/host.yml"]
init --> check["./factory validate, then plan"]
check --> apply["./factory apply"]
lock["toolchain.lock.json: sha256-pinned tools"] --> apply
lock["toolchain: latest releases, checksum-verified (omp plugins excepted)"] --> apply
apply --> profiles["Ansible profiles"]
subgraph host["Finished host"]
herdr["Herdr workspace"]
Expand Down Expand Up @@ -41,7 +41,7 @@ You need Ubuntu 24.04 or 26.04 on x86_64 or aarch64 with systemd, a non-root acc
cd Code-Factory
```

3. Install the repository tooling (pinned uv, then the locked Python environment with Ansible):
3. Install the repository tooling (the latest uv, then the locked Python environment with Ansible):

```bash
./bootstrap.sh
Expand Down Expand Up @@ -80,7 +80,7 @@ Then authenticate the agent CLIs on this account; for omp, follow [Sign in](docs
| Doc | What it covers |
| --- | --- |
| [Configuration](docs/configuration.md) | `.local/host.yml`, the `./factory` commands, and what each profile installs |
| [Dependencies](docs/dependencies.md) | Every pinned tool, package, and image, and what the host must already have |
| [Dependencies](docs/dependencies.md) | Every tool, package, and image the recipe installs, and what the host must already have |
| [Fleet guards](docs/fleet-guards.md) | Shared Supabase, Docker guard, dev-server reaper, storage guard, spawn memory floor, browser ladder |
| [Herdr sidebar](docs/herdr.md) | The Spaces and Agents sidebar layouts, what each line and token shows, the reporter timer and omp extension that feed them, and how to override them or turn parts off |
| [omp configuration](docs/omp.md) | Signing in, model roles, fallbacks, the advisor, and updating an existing host |
Expand Down
Loading