From 5b90df0ec0ba2c6b26df161644c9c0d04ae60a62 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 30 Sep 2026 23:17:24 +0000 Subject: [PATCH 01/13] feat: track latest node, bun, gh, no-mistakes, treehouse and AXI tools; drop codex and pnpm Every apply now resolves the newest release of node (nodejs.org index, not LTS), bun, gh, no-mistakes and treehouse, verified against the SHA-256 the publisher lists for that release (GitHub release-asset digest, or Node's SHASUMS256.txt), and the npm registry's latest chrome-devtools-axi, gh-axi, lavish-axi, quota-axi and tasks-axi, each in its own versioned prefix like omp. Only uv, rustup-init and the Rust toolchain stay pinned in the lock. codex and pnpm leave the npm set; acpx defaults to omp, and the installer unlinks commands whose managed package is gone. --- .github/workflows/ci.yml | 12 +- CONTRIBUTING.md | 8 +- README.md | 6 +- ansible/group_vars/all.yml | 7 +- ansible/site.yml | 2 +- ansible/tasks/agents.yml | 2 +- ansible/tasks/preflight.yml | 13 +- ansible/tasks/tools.yml | 4 +- ansible/tasks/verify.yml | 2 +- config/acpx.json | 2 +- docs/agent-host-move.md | 2 +- docs/architecture.md | 11 +- docs/configuration.md | 2 +- docs/dependencies.md | 22 +- docs/recovery.md | 2 +- maintenance/chrome-autoprune.py | 19 +- schemas/toolchain.schema.json | 18 +- scripts/factory.py | 19 +- scripts/install_tools.py | 165 ++- tests/container-smoke.sh | 83 +- tests/test_install_tools.py | 105 +- toolchain.lock.json | 114 -- tools/npm/package-lock.json | 1884 ++----------------------------- tools/npm/package.json | 11 +- 24 files changed, 423 insertions(+), 2092 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6a2529..80e00f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,17 +62,11 @@ jobs: - 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 . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 969ba34..950b724 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,8 @@ 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` — checksum-verified tool installer (idempotent) +- `toolchain.lock.json` — the pinned tools (uv, rustup-init, Rust): version, URL, sha256 - `fleet/` — runtime scripts deployed to `~/oss-fleet/` on the target host - `config/` — per-tool config templates deployed to Firstmate homes @@ -21,7 +21,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. +Only uv, rustup-init and the Rust toolchain are pinned in `toolchain.lock.json`. Every other tool tracks its latest release, resolved once per apply: native assets are verified against the SHA-256 their publisher lists for that release and refused without one, npm tools install the exact resolved version. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. No unconditional restarts, daemon-reloads, or bare commands. @@ -48,7 +48,7 @@ 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. +1. Pin it in `toolchain.lock.json` with the correct `format` (`file`, `tar` or `zip`), sha256, URL, and `binaries` map, and allow its name in `schemas/toolchain.schema.json`. A tool that tracks its latest release goes in `GITHUB_LATEST` or `NPM_LATEST` in `scripts/install_tools.py` instead. 2. Add the tool name to `factory_core_tools` in `group_vars/all.yml` (or a profile-gated list). 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). diff --git a/README.md b/README.md index 42a5e88..7eb9e83 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ⚡ 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. No Nix, no chezmoi, no cloud dependencies. ```mermaid flowchart TD @@ -8,7 +8,7 @@ flowchart TD 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"] --> apply apply --> profiles["Ansible profiles"] subgraph host["Finished host"] herdr["Herdr workspace"] @@ -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, pinned or latest, 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 | diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 9cbbca4..2d1c9b3 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -24,9 +24,10 @@ factory_lock_file: "{{ code_factory_repo }}/toolchain.lock.json" factory_pruner_source: "{{ code_factory_repo }}/maintenance/chrome-autoprune.py" factory_pruner_requirements: "{{ code_factory_repo }}/maintenance/requirements.txt" -# herdr and omp track their latest release: tasks/preflight.yml resolves them -# once per run into factory_latest, and that is what the run installs and -# tasks/verify.yml asserts. Every other version comes from the reviewed lock. +# Every tool except uv and the Rust toolchain tracks its latest release: +# tasks/preflight.yml resolves them once per run into factory_latest, and that +# is what the run installs and tasks/verify.yml asserts for herdr and omp. +# uv and Rust come from the reviewed lock. factory_herdr_version: "{{ factory_latest.herdr.version }}" factory_omp_version: "{{ factory_latest.omp }}" diff --git a/ansible/site.yml b/ansible/site.yml index 846f34b..bcad572 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -39,7 +39,7 @@ # the account work and skips what it cannot preview, instead of failing on # an owner that cannot be looked up. On a real apply the account exists by # this point and nothing is skipped. - - name: Pinned toolchain via the repository installer + - name: Toolchain via the repository installer ansible.builtin.import_tasks: tasks/tools.yml become: "{{ factory_become_target | bool }}" become_user: "{{ factory_cfg.user }}" diff --git a/ansible/tasks/agents.yml b/ansible/tasks/agents.yml index 7d24cf9..f946611 100644 --- a/ansible/tasks/agents.yml +++ b/ansible/tasks/agents.yml @@ -5,7 +5,7 @@ # once: an account that already has provider settings or an authenticated # harness keeps them (contract: "first-time writes only for OMP"). The # harness binaries themselves come from the installer's --npm set, which also -# carries the pinned gh / no-mistakes / treehouse support binaries. +# carries the gh / no-mistakes / treehouse support binaries. - name: Ensure the harness preference directories ansible.builtin.file: diff --git a/ansible/tasks/preflight.yml b/ansible/tasks/preflight.yml index 152f821..30f50ce 100644 --- a/ansible/tasks/preflight.yml +++ b/ansible/tasks/preflight.yml @@ -34,14 +34,14 @@ quiet: true when: not (factory_allow_unsupported_os | bool) -- name: Assert the CPU architecture has pinned toolchain assets +- name: Assert the CPU architecture has toolchain assets ansible.builtin.assert: that: - factory_platform | length > 0 - factory_deb_arch | length > 0 fail_msg: >- - Architecture {{ ansible_architecture }} is not supported. The lock ships - linux-x86_64 and linux-aarch64 assets only. + Architecture {{ ansible_architecture }} is not supported. The toolchain + ships linux-x86_64 and linux-aarch64 assets only. quiet: true - name: Build the list of Main-owned inputs this run requires @@ -85,15 +85,16 @@ # Resolved once per run, in plan too, so a release landing mid-run cannot # split what is installed from what the unit runs and verify asserts. The -# installer refuses a herdr release that publishes no SHA-256 for the asset. -- name: Resolve the latest herdr release and omp version +# installer refuses a native release whose publisher lists no SHA-256 for the +# asset. +- name: Resolve the latest tool releases ansible.builtin.command: argv: [python3, "{{ factory_installer }}", --lock, "{{ factory_lock_file }}", --home, "{{ factory_cfg.home }}", --resolve] register: factory_latest_run changed_when: false check_mode: false -- name: Record the resolved herdr and omp releases +- name: Record the resolved releases ansible.builtin.set_fact: factory_latest: "{{ factory_latest_run.stdout | from_json }}" diff --git a/ansible/tasks/tools.yml b/ansible/tasks/tools.yml index 2274a7d..e5dc2b2 100644 --- a/ansible/tasks/tools.yml +++ b/ansible/tasks/tools.yml @@ -1,6 +1,6 @@ --- # Toolchain, installed by the repository installer as the target user: the -# lock's pins plus the herdr and omp releases tasks/preflight.yml resolved. +# lock's pins plus the latest releases tasks/preflight.yml resolved. # # Contract: python3 /scripts/install_tools.py --lock /toolchain.lock.json # --home --tools herdr,node,bun,uv [--npm] [--development] @@ -35,7 +35,7 @@ if (factory_cfg.profiles.agents | bool) else '' }}. when: ansible_check_mode -- name: Install the pinned toolchain as the target user +- name: Install the toolchain as the target user ansible.builtin.command: argv: "{{ factory_installer_argv }}" chdir: "{{ code_factory_repo }}" diff --git a/ansible/tasks/verify.yml b/ansible/tasks/verify.yml index b2c8ee4..a305fdd 100644 --- a/ansible/tasks/verify.yml +++ b/ansible/tasks/verify.yml @@ -446,7 +446,7 @@ ~ '.'] + ['Manual: authenticate every CLI interactively as ' ~ factory_cfg.user - ~ ' (gh auth login, OMP/Codex provider sign-in); no credential was copied.'] + ~ ' (gh auth login, OMP provider sign-in); no credential was copied.'] + (['Manual: join the tailnet with `sudo tailscale up`; Tailscale SSH and the tailnet ACL stay separate decisions.'] if (factory_cfg.profiles.tailscale | bool) else []) + (['Manual: add accounts to the docker group yourself (factory_docker_group_users); it grants host root.'] diff --git a/config/acpx.json b/config/acpx.json index ee8346a..3287f3f 100644 --- a/config/acpx.json +++ b/config/acpx.json @@ -1,5 +1,5 @@ { - "defaultAgent": "codex", + "defaultAgent": "omp", "defaultPermissions": "approve-all", "nonInteractivePermissions": "deny", "authPolicy": "skip", diff --git a/docs/agent-host-move.md b/docs/agent-host-move.md index 82fda10..df32206 100644 --- a/docs/agent-host-move.md +++ b/docs/agent-host-move.md @@ -27,7 +27,7 @@ Services that hold state other hosts share, such as a model relay, a monitoring | Item | How it moves | | --- | --- | -| omp, Codex, gh and other CLI logins | Sign in again on the new host: [Sign in](omp.md#sign-in), `gh auth login`. Never copy a credential store ([Security](security.md)). | +| omp, gh and other CLI logins | Sign in again on the new host: [Sign in](omp.md#sign-in), `gh auth login`. Never copy a credential store ([Security](security.md)). | | Fleet-browser web sessions | Sign in again on the new host through the fleet-browser VNC tier: run `~/oss-fleet/browsers/fleet-browser up vnc`, then open noVNC at `http://127.0.0.1:6909/vnc.html?autoconnect=1` over an SSH tunnel (`ssh -L 6909:127.0.0.1:6909 `). `cookie-sync` then shares that session with every tier ([Browser ladder](fleet-guards.md#browser-ladder)). Sign in to Google by hand in the VNC tier; `cookie-sync` keeps Google cookies in that tier only. | | Each home's `.env` | Enter it by hand on the new host, in a file with mode `600`. | | Project clones with unpushed commits | List them on the old host with the loop below. Push each branch to its fork, except branches the [Warnings](#warnings) exclude. | diff --git a/docs/architecture.md b/docs/architecture.md index b175eda..42c54a0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,7 +15,7 @@ The source machine already uses Ubuntu packages, user-level systemd services, ho | OpenTofu | Cloud instances, networks, DNS, resource lifecycle | Add when a provider/resource contract is chosen; no pretend provider configuration is shipped | | cloud-init | Initial VM prerequisites before configuration management | Small vendor-neutral bootstrap input only | -This is repeatable configuration, not a bit-identical OS image. Ubuntu packages receive distribution security updates. Exact agent/native-tool versions are deliberately locked. Rebuilding an environment does not recreate authenticated accounts, databases, or running processes. +This is repeatable configuration, not a bit-identical OS image. Ubuntu packages receive distribution security updates. uv and the Rust toolchain are locked to exact versions; every other native and agent tool tracks its latest release, verified against its publisher's checksum, so a later rebuild can install newer versions. Rebuilding an environment does not recreate authenticated accounts, databases, or running processes. ## Host and container boundary @@ -27,7 +27,7 @@ Use a Linux host for the native recipe. macOS and other client devices can reach ### Docker worker -The Dockerfile's `worker` target is an isolated, non-root, devcontainer-style image built by the same recipe. It runs `./factory apply --config containers/factory.container.yml`, which sets `start_services: false` and `enable_linger: false` and turns off the `docker`, `tailscale`, `desktop`, and `firstmate` profiles and the browser pruner. The image carries the pinned agent and development toolchain and the rendered agent configs, with no systemd services, linger, or Docker-in-Docker. The devcontainer, Compose, and CI use the same image. +The Dockerfile's `worker` target is an isolated, non-root, devcontainer-style image built by the same recipe. It runs `./factory apply --config containers/factory.container.yml`, which sets `start_services: false` and `enable_linger: false` and turns off the `docker`, `tailscale`, `desktop`, and `firstmate` profiles and the browser pruner. The image carries the agent and development toolchain and the rendered agent configs, with no systemd services, linger, or Docker-in-Docker. The devcontainer, Compose, and CI use the same image. ```bash docker build --target worker --tag code-factory/worker . @@ -53,8 +53,8 @@ Host sizing and every auto pruner are listed in [Capacity and pruners](capacity. ## Reproducibility policy -1. Update native versions and both architecture hashes together in `toolchain.lock.json`; never resolve a mutable `latest` installer during deployment. The one exception is herdr: each apply resolves its latest release once and verifies the asset against the SHA-256 that release publishes, refusing a release without one. -2. Update exact npm dependencies and regenerate `tools/npm/package-lock.json` together. Do not copy a live global package directory. The one exception is omp, which each apply resolves to the registry's latest version and installs exactly. +1. Update the pinned native tools (uv, rustup-init) and both architecture hashes together in `toolchain.lock.json`, and the Rust toolchain version there. Every other native tool (herdr, node, bun, gh, no-mistakes, treehouse) tracks its latest release: each apply resolves it once and verifies the asset against the SHA-256 its publisher lists for that exact release (the GitHub release-asset digest, or Node's `SHASUMS256.txt`), refusing a release without one. Node is the newest release in the nodejs.org index, not the LTS line. +2. Update exact npm dependencies (acpx, chrome-devtools-mcp) and regenerate `tools/npm/package-lock.json` together. Do not copy a live global package directory. The exceptions are omp and the AXI tools (chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, tasks-axi): each apply resolves them to the registry's latest version and installs exactly that, each in its own versioned prefix. 3. Change Python dependencies with `uv lock` and commit the lock. 4. Keep machine differences in ignored `.local/host.yml`; schema validation precedes provisioning. 5. Do not force, stash, reset, or overwrite a modified Firstmate checkout or an unmanaged command. Resolve that conflict explicitly. @@ -74,7 +74,8 @@ Every action is pinned to an immutable commit SHA, and the token is read-only. T ## Primary sources - [Herdr installation](https://herdr.dev/docs/install/), [headless/SSH persistence](https://herdr.dev/docs/persistence-remote/), [session-state limits](https://herdr.dev/docs/session-state/), [config reference](https://herdr.dev/docs/config-reference/). -- [Herdr latest release](https://github.com/herdrdev/herdr/releases/latest). Assets are verified against the SHA-256 digest GitHub publishes for each release asset; no claim is made that the release supplies an independent SBOM or signature bundle. +- [Herdr latest release](https://github.com/herdrdev/herdr/releases/latest). GitHub-hosted assets (herdr, bun, gh, no-mistakes, treehouse) are verified against the SHA-256 digest GitHub publishes for each release asset; no claim is made that a release supplies an independent SBOM or signature bundle. +- [Node.js release index](https://nodejs.org/dist/index.json). Node assets are verified against the `SHASUMS256.txt` published beside each release. - [Ansible introduction](https://docs.ansible.com/projects/ansible/latest/getting_started/index.html), [checksummed downloads](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/get_url_module.html), [user systemd/D-Bus requirements](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/systemd_service_module.html). - [systemd lingering](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html). - [Docker process boundaries](https://docs.docker.com/engine/containers/multi-service_container/), [Docker and host firewall behavior](https://docs.docker.com/engine/network/firewall-iptables/). diff --git a/docs/configuration.md b/docs/configuration.md index ab4307e..8f40fb9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -78,7 +78,7 @@ The recipe refuses to overwrite a conflicting unmanaged command or an independen | Profile | Default | What it installs | | --- | --- | --- | -| `agents` | on | omp, Codex, pnpm, AXI tools, gh, no-mistakes, treehouse, acpx; safe omp presentation and model-role settings (see [omp configuration](omp.md)); first-write no-mistakes and acpx configs that make `acp:omp` the gate agent; `~/.local/bin/ponytail-review`; browser env defaults; Chrome autoprune timer. | +| `agents` | on | omp, AXI tools, gh, no-mistakes, treehouse, acpx; safe omp presentation and model-role settings (see [omp configuration](omp.md)); first-write no-mistakes and acpx configs that make `acp:omp` the gate agent; `~/.local/bin/ponytail-review`; browser env defaults; Chrome autoprune timer. | | `development` | on | Rust toolchain, build essentials, development-mode npm packages. | | `firstmate` | on | Firstmate clone tracking upstream `main`, plus seeded Firstmate config: crew dispatch, crew and secondmate harness, the crew omp overlay (crew advisor, see [omp configuration](omp.md#advisor)), Herdr backend selection, startup memory budget, and the spawn memory floor. | | `docker` | on | Docker engine and Compose v2, with daemon defaults `init` (reaps orphaned children) and `live-restore`. Group membership is opt-in through the Ansible variable `factory_docker_group_users`. | diff --git a/docs/dependencies.md b/docs/dependencies.md index cd2fc1e..7471392 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -13,23 +13,29 @@ Everything the recipe installs, grouped by the file that pins it. A version appe `toolchain.lock.json`, installed by `scripts/install_tools.py`. Every archive is sha256-locked and linked into `~/.local/bin`. -- Always: node 24.19.0, bun 1.4.0, uv 0.12.5. -- `agents` profile: gh 2.97.0, no-mistakes 1.79.0, treehouse 2.1.1. +- Always: uv 0.12.5. - `development` profile: rustup-init 1.29.0, installing Rust 1.97.1 (minimal profile + rustfmt + clippy). ## Latest releases -Not pinned: every `./factory apply` resolves the newest release, installs exactly that, and `ansible/tasks/verify.yml` asserts the installed version equals the one the run resolved, so re-running apply upgrades an existing host. The installer records the releases it installed in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against. +Not pinned: every `./factory apply` resolves the newest release once and installs exactly that, so re-running apply upgrades an existing host. The installer records the releases it installed in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. -- Always: herdr, the latest [herdrdev/herdr release](https://github.com/herdrdev/herdr/releases/latest), verified against the SHA-256 the release publishes for the platform asset. A release that publishes no checksum fails the apply instead of installing an unverified binary. The lookup uses the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it). The lookup authenticates with `GITHUB_TOKEN` from the environment that runs `./factory apply`, else runs unauthenticated. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. An upgrade rewrites and restarts `herdr.service`. -- `agents` profile: omp (`@oh-my-pi/pi-coding-agent`), the npm registry's `latest` version, installed with `npm install` into `~/.local/share/code-factory/omp/` (npm checks the registry integrity). Superseded versions stay on disk. +Native tools, linked into `~/.local/bin`. Each asset is verified against the SHA-256 its publisher lists for that exact release; a release that lists none fails the apply instead of installing an unverified binary. + +- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. +- Always: node, the newest release in the [nodejs.org index](https://nodejs.org/dist/index.json) (not the LTS line), verified against that release's `SHASUMS256.txt`. +- `agents` profile: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. + +The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); one apply makes five. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. + +npm tools, `agents` profile: the npm registry's `latest` version of omp (`@oh-my-pi/pi-coding-agent`), chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, and tasks-axi, each installed with `npm install` into `~/.local/share/code-factory//` (npm checks the registry integrity). Superseded versions stay on disk. The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. ## Agent CLIs `tools/npm/package.json`, installed with `npm ci` from `tools/npm/package-lock.json` under the `agents` profile. -- codex (`@openai/codex`) 0.147.0, pnpm 10.33.2, acpx 0.18.0 (runs the no-mistakes gate agent `acp:omp`). -- chrome-devtools-axi 0.1.29, chrome-devtools-mcp 1.9.0, gh-axi 0.1.30, lavish-axi 0.1.52, quota-axi 0.1.54, tasks-axi 0.2.6. The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. +- acpx 0.18.0 (runs the no-mistakes gate agent `acp:omp`). +- chrome-devtools-mcp 1.9.0 (the MCP build chrome-devtools-axi launches through `CHROME_DEVTOOLS_AXI_MCP_PATH`). ## Ubuntu packages @@ -73,7 +79,7 @@ Not pinned: every `./factory apply` resolves the newest release, installs exactl The recipe never installs these. The base requirements (Ubuntu, sudo, Python, `git`, `gh`) are in the [Quick start](../README.md#quick-start). Notes on those: - `bootstrap.sh` refuses to run without Python 3.12+. -- The recipe installs gh 2.97.0 later, under `agents`; the Quick start needs an authenticated `gh` before that. +- The recipe installs the latest gh later, under `agents`; the Quick start needs an authenticated `gh` before that. Also needed, depending on profile: diff --git a/docs/recovery.md b/docs/recovery.md index a0734c8..22706f5 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -56,7 +56,7 @@ Never upload a raw failed HTTP request to a public issue or this repository. Suc ## Drift and upgrades -Run `./factory plan` before applying changed pins. herdr and omp are not pinned: every apply upgrades them to their latest release, and a herdr upgrade restarts `herdr.service`. An unmanaged executable at a managed command path is a refusal, not permission to overwrite it. Firstmate refuses dirty or independently advanced checkouts. Preserve that work and decide whether to update the configuration's pin or move to a separate clean checkout. +Run `./factory plan` before applying changed pins. Only uv and the Rust toolchain are pinned: every apply upgrades every other tool to its latest release, and a herdr upgrade restarts `herdr.service`. An unmanaged executable at a managed command path is a refusal, not permission to overwrite it. Firstmate refuses dirty or independently advanced checkouts. Preserve that work and decide whether to update the configuration's pin or move to a separate clean checkout. Existing OMP settings are first-write-only: provisioning will not replace provider configuration or credentials on a reused account. Review and merge the exported safe preferences manually if deliberately updating an established account. diff --git a/maintenance/chrome-autoprune.py b/maintenance/chrome-autoprune.py index adaf18e..ca8c30b 100755 --- a/maintenance/chrome-autoprune.py +++ b/maintenance/chrome-autoprune.py @@ -34,10 +34,10 @@ HOME = Path.home() REGISTRY = HOME / ".chrome-devtools-axi" -BRIDGE = ( - HOME - / ".local/share/code-factory/npm/node_modules/chrome-devtools-axi/dist/bin/chrome-devtools-axi-bridge.js" -) +# chrome-devtools-axi tracks its latest release, one prefix per version: +# //. Bridges from a superseded version stay prunable. +AXI_ROOT = HOME / ".local/share/code-factory/chrome-devtools-axi" +BRIDGE = Path("node_modules/chrome-devtools-axi/dist/bin/chrome-devtools-axi-bridge.js") DEFAULT_STATE = HOME / ".local/state/chrome-autoprune/state.json" UID = os.getuid() @@ -46,6 +46,11 @@ class Protected(Exception): pass +def installed_bridge(path): + script, root = Path(path).resolve(), AXI_ROOT.resolve() + return script.is_relative_to(root) and script.relative_to(root).parts[1:] == BRIDGE.parts + + def owned_json(path): # Ubuntu's private-group umask creates 0775/0664 registry entries. The # registry locates candidates; live process identity/I/O authorizes stopping. @@ -73,11 +78,7 @@ def observe(name, path): if proc.uids().real != UID or proc.uids().effective != UID: raise Protected("different process owner") args = proc.cmdline() - if ( - len(args) != 2 - or Path(args[1]).resolve() != BRIDGE.resolve() - or Path(proc.exe()).name != "node" - ): + if len(args) != 2 or not installed_bridge(args[1]) or Path(proc.exe()).name != "node": raise Protected("not the installed AXI bridge") if os.getpgid(pid) != pid: raise Protected("bridge does not own its process group") diff --git a/schemas/toolchain.schema.json b/schemas/toolchain.schema.json index 16706ab..35d948e 100644 --- a/schemas/toolchain.schema.json +++ b/schemas/toolchain.schema.json @@ -8,9 +8,15 @@ }, "tools": { "type": "object", - "minProperties": 4, + "required": [ + "uv", + "rustup-init" + ], "propertyNames": { - "pattern": "^[A-Za-z0-9_.-]+$" + "enum": [ + "uv", + "rustup-init" + ] }, "additionalProperties": { "type": "object", @@ -113,13 +119,6 @@ ] } }, - "npm_required_tools": { - "type": "array", - "uniqueItems": true, - "items": { - "type": "string" - } - }, "rust_toolchain": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" @@ -128,7 +127,6 @@ "required": [ "schema_version", "tools", - "npm_required_tools", "rust_toolchain" ] } diff --git a/scripts/factory.py b/scripts/factory.py index 60fa0a4..42d9c64 100755 --- a/scripts/factory.py +++ b/scripts/factory.py @@ -49,11 +49,19 @@ def validate_config(document): if not (config["profiles"]["docker"] and config["profiles"]["firstmate"]): raise ValueError("fleet guards require the docker and firstmate profiles") browsers = config.get("browsers") - if not browsers or not browsers.get("obscura_version") or not browsers.get("obscura_sha256"): - raise ValueError("fleet_guards requires factory.browsers.obscura_version and obscura_sha256") + if ( + not browsers + or not browsers.get("obscura_version") + or not browsers.get("obscura_sha256") + ): + raise ValueError( + "fleet_guards requires factory.browsers.obscura_version and obscura_sha256" + ) sha = browsers["obscura_sha256"] if len(sha) != 64 or not all(c in "0123456789abcdef" for c in sha.lower()): - raise ValueError("factory.browsers.obscura_sha256 must be a 64-char lowercase hex string") + raise ValueError( + "factory.browsers.obscura_sha256 must be a 64-char lowercase hex string" + ) fixture = config.get("fleet", {}).get("fixture_archive", "") if fixture and ".." in Path(fixture).parts: raise ValueError("fleet.fixture_archive must not traverse; give a plain path") @@ -67,8 +75,6 @@ def validate_config(document): raise ValueError("pruning idle/gap windows must allow at least two observation intervals") lock = json.loads((ROOT / "toolchain.lock.json").read_text()) validate_document(lock, "toolchain.schema.json") - if not set(lock["npm_required_tools"]).issubset(lock["tools"]): - raise ValueError("npm support tool missing from artifact lock") return document @@ -141,7 +147,6 @@ def doctor(document): if config["profiles"]["agents"]: tools += [ "omp", - "codex", "gh", "no-mistakes", "treehouse", @@ -192,7 +197,7 @@ def doctor(document): + ("present" if authenticated else "manual gh auth login required") ) print( - "Provider access: authenticate OMP/Codex interactively; subscriptions/model availability are not inferred." + "Provider access: authenticate OMP interactively; subscriptions/model availability are not inferred." ) if config["start_services"]: print( diff --git a/scripts/install_tools.py b/scripts/install_tools.py index c222aa0..df157ac 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -4,9 +4,11 @@ Stdlib-only: also bootstraps uv before repository dependencies exist. stdout is one JSON result; installer progress goes to stderr. Existing unmanaged commands are never replaced. Archives cannot write outside their staging directory. -Every tool is pinned in the lock except herdr and omp, which track their latest -release: herdr is verified against the SHA-256 its GitHub release publishes, -omp against the integrity npm records for the resolved version. +uv, rustup-init and the Rust toolchain are pinned in the lock. Every other tool +tracks its latest release: native assets are verified against the SHA-256 their +publisher lists for that release (the GitHub release-asset digest, or Node's +SHASUMS256.txt), npm tools against the integrity npm records for the resolved +version. """ import argparse @@ -28,8 +30,42 @@ import zipfile from pathlib import Path, PurePosixPath -HERDR_LATEST = "https://api.github.com/repos/herdrdev/herdr/releases/latest" -OMP_PACKAGE = "@oh-my-pi/pi-coding-agent" +GITHUB_API = "https://api.github.com/repos/{}/releases/latest" +# How release asset names spell each platform. +ARCH = { + "linux-x86_64": {"node": "x64", "go": "amd64", "bun": "x64-baseline"}, + "linux-aarch64": {"node": "arm64", "go": "arm64", "bun": "aarch64"}, +} +# Native tools on their latest GitHub release: repository, tag prefix, asset +# name, and where each command sits inside the asset. +GITHUB_LATEST = { + "herdr": ("herdrdev/herdr", "v", "herdr-{key}", {"herdr": "herdr"}), + "bun": ("oven-sh/bun", "bun-v", "bun-linux-{bun}.zip", {"bun": "bun-linux-*/bun"}), + "gh": ("cli/cli", "v", "gh_{v}_linux_{go}.tar.gz", {"gh": "gh_*/bin/gh"}), + "no-mistakes": ( + "kunchenguid/no-mistakes", + "v", + "no-mistakes-v{v}-linux-{go}.tar.gz", + {"no-mistakes": "no-mistakes"}, + ), + "treehouse": ( + "kunchenguid/treehouse", + "v", + "treehouse-v{v}-linux-{go}.tar.gz", + {"treehouse": "treehouse"}, + ), +} +# npm tools on the registry's latest version, each installed into its own prefix. +NPM_LATEST = { + "omp": "@oh-my-pi/pi-coding-agent", + "chrome-devtools-axi": "chrome-devtools-axi", + "gh-axi": "gh-axi", + "lavish-axi": "lavish-axi", + "quota-axi": "quota-axi", + "tasks-axi": "tasks-axi", +} +# Native tools the agents profile adds. +AGENT_TOOLS = ["gh", "no-mistakes", "treehouse"] def digest(path): @@ -48,45 +84,65 @@ def platform_key(): return "linux-" + machine -def resolve_latest(key): - """The newest herdr release as a lock-shaped spec, and the newest omp version.""" - # Unauthenticated GitHub API calls share a 60/hour budget per IP. +def fetch(url, what): + # The GitHub token only ever goes to the GitHub API. Unauthenticated calls + # there share a 60/hour budget per IP. token = os.environ.get("GITHUB_TOKEN") - headers = {"Authorization": f"Bearer {token}"} if token else {} + github = url.startswith("https://api.github.com/") + headers = {"Authorization": f"Bearer {token}"} if token and github else {} try: with urllib.request.urlopen( - urllib.request.Request(HERDR_LATEST, headers=headers), timeout=60 + urllib.request.Request(url, headers=headers), timeout=60 ) as response: - release = json.load(response) + return response.read() except urllib.error.HTTPError as error: raise ValueError( - f"cannot resolve the latest herdr release (HTTP {error.code}); " + f"cannot resolve the latest {what} (HTTP {error.code}); " "if rate limited, set GITHUB_TOKEN and re-run" ) from None - version = release["tag_name"].removeprefix("v") - name = f"herdr-{key}" - asset = next((a for a in release["assets"] if a["name"] == name), {}) - checksum = (asset.get("digest") or "").removeprefix("sha256:") + + +def verified(tool, version, key, name, url, checksum, binaries): + """A lock-shaped spec, only when the publisher lists a SHA-256 for the asset.""" if not re.fullmatch(r"[0-9a-f]{64}", checksum): raise ValueError( - f"herdr {version} publishes no SHA-256 for {name}; refusing an unverified binary" + f"{tool} {version} publishes no SHA-256 for {name}; refusing an unverified binary" ) - herdr = { - "version": version, - "assets": { - key: { - "url": asset["browser_download_url"], - "sha256": checksum, - "format": "file", - "binaries": {"herdr": "herdr"}, - } - }, - } - with urllib.request.urlopen( - f"https://registry.npmjs.org/{OMP_PACKAGE}/latest", timeout=60 - ) as response: - omp = json.load(response)["version"] - return {"herdr": herdr, "omp": omp} + kind = "zip" if name.endswith(".zip") else "tar" if ".tar." in name else "file" + asset = {"url": url, "sha256": checksum, "format": kind, "binaries": binaries} + return {"version": version, "assets": {key: asset}} + + +def resolve_latest(key): + """Specs for the newest native releases, and the newest npm tool versions.""" + latest = {} + for tool, (repo, prefix, pattern, binaries) in GITHUB_LATEST.items(): + release = json.loads(fetch(GITHUB_API.format(repo), f"{tool} release")) + version = release["tag_name"].removeprefix(prefix) + name = pattern.format(v=version, key=key, **ARCH[key]) + asset = next((a for a in release["assets"] if a["name"] == name), {}) + checksum = (asset.get("digest") or "").removeprefix("sha256:") + url = asset.get("browser_download_url", "") + latest[tool] = verified(tool, version, key, name, url, checksum, binaries) + releases = json.loads(fetch("https://nodejs.org/dist/index.json", "node release")) + tag = max((r["version"] for r in releases), key=lambda v: tuple(map(int, v[1:].split(".")))) + name = f"node-{tag}-linux-{ARCH[key]['node']}.tar.xz" + sums = fetch(f"https://nodejs.org/dist/{tag}/SHASUMS256.txt", "node checksums").decode() + match = re.search(rf"^([0-9a-f]{{64}}) {re.escape(name)}$", sums, re.M) + binaries = {command: f"node-v*/bin/{command}" for command in ("node", "npm", "npx")} + latest["node"] = verified( + "node", + tag.removeprefix("v"), + key, + name, + f"https://nodejs.org/dist/{tag}/{name}", + match[1] if match else "", + binaries, + ) + for tool, package in NPM_LATEST.items(): + registry = fetch(f"https://registry.npmjs.org/{package}/latest", f"{tool} version") + latest[tool] = json.loads(registry)["version"] + return latest def download(url, checksum, destination): @@ -260,18 +316,19 @@ def link_package_bins(home, destination, package_names): return changed -def omp_install(home, version, environment): - """Install exactly this omp version into its own prefix; a new version relinks omp.""" +def npm_latest_install(home, tool, version, environment): + """Install exactly this version into its own prefix; a new version relinks the tool.""" + package_name = NPM_LATEST[tool] if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+[A-Za-z0-9.+-]*", version): - raise ValueError("unsafe omp version") + raise ValueError(f"unsafe {tool} version") # ponytail: superseded versions stay on disk so running agents keep their files; # prune them by hand or add a sweep if disk use matters. - final = home / ".local/share/code-factory/omp" / version - package = final / "node_modules" / OMP_PACKAGE / "package.json" + final = home / ".local/share/code-factory" / tool / version + package = final / "node_modules" / package_name / "package.json" changed = False if not package.is_file() or json.loads(package.read_text()).get("version") != version: if final.exists(): - raise ValueError(f"incomplete omp install: {final}; inspect it before replacing") + raise ValueError(f"incomplete {tool} install: {final}; inspect it before replacing") final.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".install-", dir=final.parent) as temporary: staging = Path(temporary) / "prefix" @@ -284,13 +341,24 @@ def omp_install(home, version, environment): staging, "--no-audit", "--no-fund", - f"{OMP_PACKAGE}@{version}", + f"{package_name}@{version}", ], environment, ) staging.rename(final) changed = True - return link_package_bins(home, final, [OMP_PACKAGE]) or changed + return link_package_bins(home, final, [package_name]) or changed + + +def prune_dangling_links(home): + """Remove managed commands whose package is no longer installed.""" + changed = False + for link in (home / ".local/bin").iterdir(): + if link.is_symlink() and not link.exists(): + if Path(os.readlink(link)).is_relative_to(home / ".local/share/code-factory"): + link.unlink() + changed = True + return changed def rust_install(home, version, environment): @@ -385,7 +453,7 @@ def main(): parser.add_argument("--npm", action="store_true") parser.add_argument("--development", action="store_true") parser.add_argument( - "--resolve", action="store_true", help="print the latest herdr and omp as JSON and exit" + "--resolve", action="store_true", help="print the latest releases as JSON and exit" ) parser.add_argument( "--resolved", type=json.loads, help="--resolve output to install instead of resolving" @@ -401,12 +469,11 @@ def main(): lock = json.loads(args.lock.read_text()) if lock.get("schema_version") != 1: parser.error("unsupported toolchain lock schema") - names = list(dict.fromkeys(args.tools.split(","))) - if args.npm: - names = list(dict.fromkeys([*names, *lock["npm_required_tools"]])) + names = list(dict.fromkeys(args.tools.split(",") + (AGENT_TOOLS if args.npm else []))) if args.development: names.append("rustup-init") - latest = args.resolved or (resolve_latest(key) if "herdr" in names or args.npm else {}) + wanted = args.npm or any(name == "node" or name in GITHUB_LATEST for name in names) + latest = args.resolved or (resolve_latest(key) if wanted else {}) environment = { **os.environ, "HOME": str(home), @@ -418,11 +485,13 @@ def main(): fcntl.flock(guard, fcntl.LOCK_EX) changed = False for name in names: - spec = latest["herdr"] if name == "herdr" else lock["tools"][name] + spec = latest[name] if name in latest else lock["tools"][name] changed = install_asset(home, name, spec, key) or changed if args.npm: changed = npm_install(args.lock.resolve().parent, home, environment) or changed - changed = omp_install(home, latest["omp"], environment) or changed + for tool in NPM_LATEST: + changed = npm_latest_install(home, tool, latest[tool], environment) or changed + changed = prune_dangling_links(home) or changed if args.development: changed = rust_install(home, lock["rust_toolchain"], environment) or changed # The record checks compare against, so a later upstream release cannot diff --git a/tests/container-smoke.sh b/tests/container-smoke.sh index 319c0ff..e741078 100755 --- a/tests/container-smoke.sh +++ b/tests/container-smoke.sh @@ -212,9 +212,10 @@ lock_version() { jq -er --arg tool "$1" '.tools[$tool].version' "${CF_ROOT}/toolchain.lock.json" } -# herdr and omp track their latest release. The installer records the release -# it installed in RESOLVED_STAMP; checks compare against that record, so an -# upstream release published after the build cannot turn them red. +# Every tool except uv and the Rust toolchain tracks its latest release. The +# installer records the releases it installed in RESOLVED_STAMP; checks compare +# against that record, so an upstream release published after the build cannot +# turn them red. RESOLVED_STAMP="${HOME}/.local/share/code-factory/resolved.json" resolved_version() { jq -er "$1" "${RESOLVED_STAMP}" @@ -230,13 +231,13 @@ herdr_installed_bin() { # A second apply resolves upstream again and upgrades an image that upstream # has since overtaken; that is an upgrade, not a repeat change. image_is_current() { - local latest + local latest versions='map_values(.version? // .)' latest=$(python3 "${CF_ROOT}/scripts/install_tools.py" --lock "${CF_ROOT}/toolchain.lock.json" \ - --home "${HOME}" --resolve) || fail "could not resolve the latest herdr and omp releases" - [ "$(jq -c '[.herdr.version, .omp]' <<<"${latest}")" = "$(jq -c '[.herdr.version, .omp]' "${RESOLVED_STAMP}")" ] \ + --home "${HOME}" --resolve) || fail "could not resolve the latest releases" + [ "$(jq -cS "${versions}" <<<"${latest}")" = "$(jq -cS "${versions}" "${RESOLVED_STAMP}")" ] \ && return 0 printf 'image has %s but upstream now has %s; idempotence not measured\n' \ - "$(jq -c '[.herdr.version, .omp]' "${RESOLVED_STAMP}")" "$(jq -c '[.herdr.version, .omp]' <<<"${latest}")" + "$(jq -cS "${versions}" "${RESOLVED_STAMP}")" "$(jq -cS "${versions}" <<<"${latest}")" return 1 } @@ -319,7 +320,7 @@ check_workspace() { } check_core_tools() { - local tool bin locked version + local tool bin expected version for tool in node bun uv; do bin="${HOME}/.local/bin/${tool}" [ -x "${bin}" ] || fail "missing managed launcher ${bin}" @@ -327,36 +328,48 @@ check_core_tools() { version=$("${bin}" --version 2>&1) || fail "${tool} --version exited nonzero: ${version}" version=${version%%$'\n'*} [ -n "${version}" ] || fail "${tool} --version produced no output" - # An existing command is not enough: it must be the build the lock pins. - locked=$(lock_version "${tool}") || fail "toolchain.lock.json pins no version for ${tool}" + # An existing command is not enough: it must be the build the lock pins + # (uv) or the release the build resolved (node, bun). + if [ "${tool}" = uv ]; then + expected=$(lock_version uv) || fail "toolchain.lock.json pins no version for uv" + else + expected=$(resolved_version ".${tool}.version") || fail "${RESOLVED_STAMP} records no ${tool}" + fi case "${version}" in - *"${locked}"*) ;; - *) fail "${tool} reports '${version}' but the lock pins ${locked}" ;; + *"${expected}"*) ;; + *) fail "${tool} reports '${version}' but expected ${expected}" ;; esac - printf '%-6s %-32s (lock %s)\n' "${tool}" "${version}" "${locked}" + printf '%-6s %-32s (expected %s)\n' "${tool}" "${version}" "${expected}" done } -# The installed herdr and omp are exactly the releases the build resolved and -# recorded in RESOLVED_STAMP. +# The installed herdr, gh, no-mistakes, treehouse, omp and AXI tools are exactly +# the releases the build resolved and recorded in RESOLVED_STAMP. check_resolved_releases() { - local herdr omp banner prefix + local tool version banner package prefix [ -f "${RESOLVED_STAMP}" ] || fail "installer wrote no ${RESOLVED_STAMP}" - herdr=$(resolved_version .herdr.version) - omp=$(resolved_version .omp) - banner=$("${HOME}/.local/bin/herdr" --version 2>&1) || fail "herdr --version exited nonzero: ${banner}" - case "${banner%%$'\n'*}" in - *"${herdr}"*) ;; - *) fail "herdr reports '${banner%%$'\n'*}' but the build resolved ${herdr}" ;; - esac - prefix="${HOME}/.local/share/code-factory/omp/${omp}" - case "$(readlink -f "${HOME}/.local/bin/omp")" in - "${prefix}"/*) ;; - *) fail "omp resolves to $(readlink -f "${HOME}/.local/bin/omp"), expected the resolved release under ${prefix}" ;; - esac - [ "$(jq -r .version "${prefix}/node_modules/@oh-my-pi/pi-coding-agent/package.json")" = "${omp}" ] \ - || fail "omp under ${prefix} is not version ${omp}" - printf 'herdr %s and omp %s match %s\n' "${herdr}" "${omp}" "${RESOLVED_STAMP}" + for tool in herdr gh no-mistakes treehouse; do + version=$(resolved_version ".\"${tool}\".version") + banner=$("${HOME}/.local/bin/${tool}" --version 2>&1) || fail "${tool} --version exited nonzero: ${banner}" + case "${banner%%$'\n'*}" in + *"${version}"*) ;; + *) fail "${tool} reports '${banner%%$'\n'*}' but the build resolved ${version}" ;; + esac + printf '%s %s\n' "${tool}" "${version}" + done + for tool in omp:@oh-my-pi/pi-coding-agent chrome-devtools-axi gh-axi lavish-axi quota-axi tasks-axi; do + package=${tool#*:}; tool=${tool%%:*} + version=$(resolved_version ".\"${tool}\"") + prefix="${HOME}/.local/share/code-factory/${tool}/${version}" + case "$(readlink -f "${HOME}/.local/bin/${tool}")" in + "${prefix}"/*) ;; + *) fail "${tool} resolves to $(readlink -f "${HOME}/.local/bin/${tool}"), expected the resolved release under ${prefix}" ;; + esac + [ "$(jq -r .version "${prefix}/node_modules/${package}/package.json")" = "${version}" ] \ + || fail "${tool} under ${prefix} is not version ${version}" + printf '%s %s\n' "${tool}" "${version}" + done + printf 'installed releases match %s\n' "${RESOLVED_STAMP}" } check_herdr_install_layout() { @@ -740,10 +753,10 @@ check_login_shell_environment() { # What the no-mistakes gate and the PR review need on a fresh host: acp:omp as # the gate agent with acpx mapping omp to `omp acp`, no omp registry override, # ponytail-review's exit codes against a stub omp, and the fleet's minimum AXI -# versions. Versions come from the installed package.json, so no agent CLI is executed. +# versions. Versions come from RESOLVED_STAMP, which resolved-releases matched against +# the installed package.json, so no agent CLI is executed. check_agent_gate() { - local npm_root name floor version stub row want omp_rc omp_out rc - npm_root="${HOME}/.local/share/code-factory/npm" + local name floor version stub row want omp_rc omp_out rc cf_python -c 'import sys, yaml; c = yaml.safe_load(open(sys.argv[1])); assert c["agent"] == ["acp:omp"] and "acp_registry_overrides" not in c' \ "${HOME}/.no-mistakes/config.yaml" || fail "~/.no-mistakes/config.yaml does not make acp:omp the gate agent" jq -e '.agents.omp.command == "omp acp"' "${HOME}/.acpx/config.json" >/dev/null || fail "~/.acpx/config.json does not map omp to omp acp" @@ -773,7 +786,7 @@ check_agent_gate() { trap - EXIT for name in quota-axi:0.1.54 tasks-axi:0.2.6; do floor=${name#*:}; name=${name%%:*} - version=$(jq -r .version "${npm_root}/node_modules/${name}/package.json") + version=$(resolved_version ".\"${name}\"") printf '%s\n%s\n' "${floor}" "${version}" | sort -C -V || fail "${name} ${version} is below the fleet floor ${floor}" printf '%s %s (floor %s)\n' "${name}" "${version}" "${floor}" done diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index c49515f..218c824 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -96,50 +96,89 @@ def test_download_rejects_plain_http(tmp_path): installer.download("http://example.test/tool", "0" * 64, tmp_path / "download") -def release(digest): - asset = { - "name": "herdr-linux-x86_64", - "browser_download_url": "https://github.com/herdrdev/herdr/releases/download/v9.9.9/herdr-linux-x86_64", - "digest": digest, - } - return {"tag_name": "v9.9.9", "assets": [asset]} - - -def registries(monkeypatch, herdr_release): - def urlopen(url, **kwargs): - url = getattr(url, "full_url", url) - body = herdr_release if url == installer.HERDR_LATEST else {"version": "18.9.9"} +# What each fake GitHub release publishes for linux-x86_64: tag and asset name. +RELEASES = { + "herdrdev/herdr": ("v9.9.9", "herdr-linux-x86_64"), + "oven-sh/bun": ("bun-v9.9.9", "bun-linux-x64-baseline.zip"), + "cli/cli": ("v9.9.9", "gh_9.9.9_linux_amd64.tar.gz"), + "kunchenguid/no-mistakes": ("v9.9.9", "no-mistakes-v9.9.9-linux-amd64.tar.gz"), + "kunchenguid/treehouse": ("v9.9.9", "treehouse-v9.9.9-linux-amd64.tar.gz"), +} + + +def upstream(monkeypatch, unverified=None, seen=None): + """Fake GitHub, nodejs.org and npm; `unverified` publishes no SHA-256 for that source.""" + + def urlopen(request, **kwargs): + url = request.full_url + if seen is not None: + seen.append((url, request.get_header("Authorization"))) + if url.startswith("https://api.github.com/repos/"): + repo = url.removeprefix("https://api.github.com/repos/").removesuffix( + "/releases/latest" + ) + tag, name = RELEASES[repo] + digest = None if repo == unverified else "sha256:" + "a" * 64 + asset = { + "name": name, + "browser_download_url": f"https://github.com/{name}", + "digest": digest, + } + body = {"tag_name": tag, "assets": [asset]} + elif url == "https://nodejs.org/dist/index.json": + body = [{"version": "v9.99.0"}, {"version": "v30.1.0"}] + elif url == "https://nodejs.org/dist/v30.1.0/SHASUMS256.txt": + sums = "" if unverified == "node" else "b" * 64 + " node-v30.1.0-linux-x64.tar.xz\n" + return io.BytesIO(("c" * 64 + " node-v30.1.0-linux-arm64.tar.xz\n" + sums).encode()) + else: + body = {"version": "18.9.9"} return io.BytesIO(json.dumps(body).encode()) monkeypatch.setattr(installer.urllib.request, "urlopen", urlopen) -def test_latest_herdr_is_pinned_to_the_digest_its_release_publishes(monkeypatch): - registries(monkeypatch, release("sha256:" + "a" * 64)) +def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeypatch): + upstream(monkeypatch) latest = installer.resolve_latest("linux-x86_64") - assert latest["omp"] == "18.9.9" - assert latest["herdr"]["version"] == "9.9.9" - assert latest["herdr"]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 - - -@pytest.mark.parametrize("digest", [None, "", "md5:abc"]) -def test_herdr_release_without_a_checksum_is_refused(monkeypatch, digest): - registries(monkeypatch, release(digest)) + for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse"): + assert latest[tool]["version"] == "9.9.9" + assert latest[tool]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 + assert latest["gh"]["assets"]["linux-x86_64"]["format"] == "tar" + assert latest["bun"]["assets"]["linux-x86_64"]["format"] == "zip" + # The newest Node release, not the first index entry, verified by SHASUMS256. + node = latest["node"]["assets"]["linux-x86_64"] + assert latest["node"]["version"] == "30.1.0" + assert node["url"] == "https://nodejs.org/dist/v30.1.0/node-v30.1.0-linux-x64.tar.xz" + assert node["sha256"] == "b" * 64 + for tool in ("omp", "chrome-devtools-axi", "gh-axi", "lavish-axi", "quota-axi", "tasks-axi"): + assert latest[tool] == "18.9.9" + + +@pytest.mark.parametrize("source", [*RELEASES, "node"]) +def test_release_without_a_published_checksum_is_refused(monkeypatch, source): + upstream(monkeypatch, unverified=source) with pytest.raises(ValueError, match="refusing an unverified binary"): installer.resolve_latest("linux-x86_64") @pytest.mark.parametrize(("env", "expected"), [("env-token", "Bearer env-token"), ("", None)]) -def test_herdr_lookup_authenticates_with_github_token_else_anonymous(monkeypatch, env, expected): +def test_github_token_goes_only_to_the_github_api(monkeypatch, env, expected): seen = [] - - def urlopen(url, **kwargs): - if getattr(url, "full_url", url) == installer.HERDR_LATEST: - seen.append(url.get_header("Authorization")) - return io.BytesIO(json.dumps(release("sha256:" + "a" * 64)).encode()) - return io.BytesIO(json.dumps({"version": "18.9.9"}).encode()) - monkeypatch.setenv("GITHUB_TOKEN", env) - monkeypatch.setattr(installer.urllib.request, "urlopen", urlopen) + upstream(monkeypatch, seen=seen) installer.resolve_latest("linux-x86_64") - assert seen == [expected] + github = {auth for url, auth in seen if url.startswith("https://api.github.com/")} + assert github == {expected} + assert {auth for url, auth in seen if not url.startswith("https://api.github.com/")} == {None} + + +def test_commands_of_removed_packages_are_unlinked_but_user_links_kept(tmp_path): + bin_dir = tmp_path / ".local/bin" + bin_dir.mkdir(parents=True) + managed = bin_dir / "codex" + managed.symlink_to(tmp_path / ".local/share/code-factory/npm/node_modules/gone/bin.js") + user = bin_dir / "mine" + user.symlink_to(tmp_path / "elsewhere/gone") + assert installer.prune_dangling_links(tmp_path) + assert not managed.is_symlink() + assert user.is_symlink() diff --git a/toolchain.lock.json b/toolchain.lock.json index 149f0c9..3dd0344 100644 --- a/toolchain.lock.json +++ b/toolchain.lock.json @@ -1,52 +1,6 @@ { "schema_version": 1, "tools": { - "node": { - "version": "24.19.0", - "assets": { - "linux-x86_64": { - "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.xz", - "sha256": "14b342e71204f811bde6153be8e04b62aef63c236fef92b55f9c83154b409647", - "format": "tar", - "binaries": { - "node": "node-v24.19.0-linux-x64/bin/node", - "npm": "node-v24.19.0-linux-x64/bin/npm", - "npx": "node-v24.19.0-linux-x64/bin/npx" - } - }, - "linux-aarch64": { - "url": "https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-arm64.tar.xz", - "sha256": "01443c1e1a29e531ccad5a46fefa6df490d2189c49f7955904aecdbb0fe86fdc", - "format": "tar", - "binaries": { - "node": "node-v24.19.0-linux-arm64/bin/node", - "npm": "node-v24.19.0-linux-arm64/bin/npm", - "npx": "node-v24.19.0-linux-arm64/bin/npx" - } - } - } - }, - "bun": { - "version": "1.4.0", - "assets": { - "linux-x86_64": { - "url": "https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-x64-baseline.zip", - "sha256": "184fb4595f0d401a217cf7c78c1bc430ba83314dab7a8b94805babbf7fa7097f", - "format": "zip", - "binaries": { - "bun": "bun-linux-x64-baseline/bun" - } - }, - "linux-aarch64": { - "url": "https://github.com/oven-sh/bun/releases/download/bun-v1.4.0/bun-linux-aarch64.zip", - "sha256": "4b1a332ee861983eb93bcfe6f770fff94e3e31b2c388bdaea3c8ed35e58eed0e", - "format": "zip", - "binaries": { - "bun": "bun-linux-aarch64/bun" - } - } - } - }, "uv": { "version": "0.12.5", "assets": { @@ -70,69 +24,6 @@ } } }, - "gh": { - "version": "2.97.0", - "assets": { - "linux-x86_64": { - "url": "https://github.com/cli/cli/releases/download/v2.97.0/gh_2.97.0_linux_amd64.tar.gz", - "sha256": "a2c9b8497e1f85b1ad0dfcb78b5a622e098801b8e461e459e88e1ee12f018112", - "format": "tar", - "binaries": { - "gh": "gh_2.97.0_linux_amd64/bin/gh" - } - }, - "linux-aarch64": { - "url": "https://github.com/cli/cli/releases/download/v2.97.0/gh_2.97.0_linux_arm64.tar.gz", - "sha256": "73ea440ecad9c9e284429997ee6f93577bc6f7bc6fba357ef62c53ad8fb641a5", - "format": "tar", - "binaries": { - "gh": "gh_2.97.0_linux_arm64/bin/gh" - } - } - } - }, - "no-mistakes": { - "version": "1.79.0", - "assets": { - "linux-x86_64": { - "url": "https://github.com/kunchenguid/no-mistakes/releases/download/v1.79.0/no-mistakes-v1.79.0-linux-amd64.tar.gz", - "sha256": "d178c8a5134763b8e5f6d82545a3a76285fbbcdc13d09020d1091ec80a3f8da6", - "format": "tar", - "binaries": { - "no-mistakes": "no-mistakes" - } - }, - "linux-aarch64": { - "url": "https://github.com/kunchenguid/no-mistakes/releases/download/v1.79.0/no-mistakes-v1.79.0-linux-arm64.tar.gz", - "sha256": "eac3f8e494c7d2487594b7eb513e605e59588be4ce147390b8fd184a7e6a8b05", - "format": "tar", - "binaries": { - "no-mistakes": "no-mistakes" - } - } - } - }, - "treehouse": { - "version": "2.1.1", - "assets": { - "linux-x86_64": { - "url": "https://github.com/kunchenguid/treehouse/releases/download/v2.1.1/treehouse-v2.1.1-linux-amd64.tar.gz", - "sha256": "2fe3e01220ae51a967c3e5ba6ccf10ec83bdbae8e420368d194285a8d04c9ef8", - "format": "tar", - "binaries": { - "treehouse": "treehouse" - } - }, - "linux-aarch64": { - "url": "https://github.com/kunchenguid/treehouse/releases/download/v2.1.1/treehouse-v2.1.1-linux-arm64.tar.gz", - "sha256": "980367c0233274eb3181a19a2ca8ec69d09b4a588ba27367937d336f9a2c938e", - "format": "tar", - "binaries": { - "treehouse": "treehouse" - } - } - } - }, "rustup-init": { "version": "1.29.0", "assets": { @@ -155,10 +46,5 @@ } } }, - "npm_required_tools": [ - "gh", - "no-mistakes", - "treehouse" - ], "rust_toolchain": "1.97.1" } diff --git a/tools/npm/package-lock.json b/tools/npm/package-lock.json index 12eb268..e0f2e95 100644 --- a/tools/npm/package-lock.json +++ b/tools/npm/package-lock.json @@ -8,15 +8,8 @@ "name": "code-factory-agent-tools", "version": "1.0.0", "dependencies": { - "@openai/codex": "0.147.0", "acpx": "0.18.0", - "chrome-devtools-axi": "0.1.29", - "chrome-devtools-mcp": "1.9.0", - "gh-axi": "0.1.30", - "lavish-axi": "0.1.52", - "pnpm": "10.33.2", - "quota-axi": "0.1.54", - "tasks-axi": "0.2.6" + "chrome-devtools-mcp": "1.9.0" } }, "node_modules/@agentclientprotocol/sdk": { @@ -472,180 +465,6 @@ "node": ">=18" } }, - "node_modules/@hono/node-server": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", - "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", - "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9 || ^2.0.5", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@openai/codex": { - "version": "0.147.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0.tgz", - "integrity": "sha512-EQLEXecAG2ptxI7UpBMo2TR/ga5596/c/OsYF/0LoUDh5JANZ7IoGqlzBEWbuEVQ76JePIbtTW/ihCkp1a7Z3w==", - "license": "Apache-2.0", - "bin": { - "codex": "bin/codex.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.147.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.147.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.147.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.147.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.147.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.147.0-win32-x64" - } - }, - "node_modules/@openai/codex-darwin-arm64": { - "name": "@openai/codex", - "version": "0.147.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0-darwin-arm64.tgz", - "integrity": "sha512-BEUVkiOW7kLcRyrMLfAr/h9wF8sRVJyZDy6OHtVn6QGDXiv3BvAZVTY1Pu9xF7KdIdkYXbp4uayN0aDQQaAUJw==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-darwin-x64": { - "name": "@openai/codex", - "version": "0.147.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0-darwin-x64.tgz", - "integrity": "sha512-Tb8McE5SvJIH0Vs5R6sq7u+quiC931yan2KOOl6km1OdZ82+Wi7eF5XrSFPs5CF7xCgoIK4Vs+byMbT5hN+ZUw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-linux-arm64": { - "name": "@openai/codex", - "version": "0.147.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0-linux-arm64.tgz", - "integrity": "sha512-SLC1JXw2TYfr/c3HhrJubyyLelq7vTOLWVmiThFA+z0+WgzCPmaseJ/kzDD3Gge/TO7fCnnj7UcPmC0d2c8XAg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-linux-x64": { - "name": "@openai/codex", - "version": "0.147.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0-linux-x64.tgz", - "integrity": "sha512-0W9MBxPpWW0cSkNqrTDN2jR7rzzT7oNMhQY5446lT2Lw5cz5yhDTck4Va9rjkQEm+HlFzP/dmEMSZbXfJsINmw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-win32-arm64": { - "name": "@openai/codex", - "version": "0.147.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0-win32-arm64.tgz", - "integrity": "sha512-e2ZstJ8zT8Rm1nvR7CUVO+Gr3cTChE41+VfOzGhynzDXEoW0wfbjUQbc2bWbh1arG94LMm4y3dqBtUIbSrfeGA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-win32-x64": { - "name": "@openai/codex", - "version": "0.147.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.147.0-win32-x64.tgz", - "integrity": "sha512-oT7Ss5fAPf2fiWE9QNURqZcQGAAawSVxmIUdgPzckq4KFZAM+pRz9JbM4Rr498CjtbNgTOjWvDJ+DXvIBSfOPA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "node_modules/@openclaw/fs-safe": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.12.0.tgz", @@ -789,25 +608,6 @@ "node": ">=22" } }, - "node_modules/@tailwindcss/browser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/browser/-/browser-4.2.4.tgz", - "integrity": "sha512-yd+3CxxuF1KDt9Q+405JR3/vyotvS5eMIsZPEynEak/JybvFVn8mVmLjVUxgNmrFB6EGCc89lXBECzIZA+YeXQ==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/acpx": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/acpx/-/acpx-0.18.0.tgz", @@ -828,57 +628,6 @@ "node": ">=22.13.0" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/axi-sdk-js": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/axi-sdk-js/-/axi-sdk-js-0.1.11.tgz", - "integrity": "sha512-ksACrhe4GdsIK/TfPz8P0iA8NmiRFQ43zu3/HM7NGPv7s1+xKUsupufXntedR4YxM23S2zMPJyLvn/jS/wZyBg==", - "license": "MIT", - "dependencies": { - "@toon-format/toon": "^2.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/axi-sdk-js/node_modules/@toon-format/toon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz", - "integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==", - "license": "MIT" - }, "node_modules/b4a": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", @@ -973,134 +722,6 @@ "bare-path": "^3.0.0" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/chrome-devtools-axi": { - "version": "0.1.29", - "resolved": "https://registry.npmjs.org/chrome-devtools-axi/-/chrome-devtools-axi-0.1.29.tgz", - "integrity": "sha512-ILnqDZbAPylESC4tWsAPZNhzL1qXFsOeRO4D5YEyhAHAq3ulBfb43s4ZjLAZ+ykeJEi8XGyqVcX/MROhguuA9w==", - "license": "MIT", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", - "@toon-format/toon": "^2.1.0", - "axi-sdk-js": "^0.1.10" - }, - "bin": { - "chrome-devtools-axi": "dist/bin/chrome-devtools-axi.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/chrome-devtools-axi/node_modules/@toon-format/toon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz", - "integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==", - "license": "MIT" - }, "node_modules/chrome-devtools-mcp": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-1.9.0.tgz", @@ -1135,46 +756,6 @@ "node": ">=22.12.0" } }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -1182,337 +763,56 @@ "license": "MIT", "optional": true }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": ">= 0.10" + "node": ">=18" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "bare-events": "^2.7.0" } }, - "node_modules/daisyui": { - "version": "5.7.38", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.38.tgz", - "integrity": "sha512-ef3RIbteKVmlQ10ak0l4mu8F70pBJ2UBjDJNsA2vWlpNdErpHqfxddxK2ckNNYd7vQvTqP4a1A1vSsunlCGVOw==", - "license": "MIT", - "funding": { - "url": "https://github.com/saadeghi/daisyui?sponsor=1" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/default-browser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", - "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", - "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", - "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", - "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -1527,726 +827,93 @@ }, "node_modules/fast-string-width": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", - "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gh-axi": { - "version": "0.1.30", - "resolved": "https://registry.npmjs.org/gh-axi/-/gh-axi-0.1.30.tgz", - "integrity": "sha512-4qw7+INJqdH5obm6NOUQnqBRALMG/BYQwTseVr9I7DHvccEytBtltc0EvB0SxrDzIUTKPshI9uKtfp83TjlBjA==", - "license": "MIT", - "dependencies": { - "@toon-format/toon": "^2.1.0", - "axi-sdk-js": "^0.1.10" - }, - "bin": { - "gh-axi": "dist/bin/gh-axi.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/gh-axi/node_modules/@toon-format/toon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz", - "integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==", - "license": "MIT" - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.7.tgz", - "integrity": "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT", - "optional": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT", - "optional": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/jszip": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", - "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", - "license": "(MIT OR GPL-3.0-or-later)", - "optional": true, - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lavish-axi": { - "version": "0.1.52", - "resolved": "https://registry.npmjs.org/lavish-axi/-/lavish-axi-0.1.52.tgz", - "integrity": "sha512-obBN6jNnIra7v+pIik8dtfL2YqcfLYRwew44yshtrxuMRq9E21EuWDf0zWvJItJEXcptqAo6PEyX5gutgm+Pow==", - "license": "MIT", - "dependencies": { - "@tailwindcss/browser": "4.2.4", - "axi-sdk-js": "^0.1.8", - "chokidar": "^4.0.3", - "cross-spawn": "7.0.6", - "daisyui": "^5.5.19", - "express": "^5.2.1", - "open": "^10.2.0", - "parse5": "^8.0.1" - }, - "bin": { - "lavish-axi": "dist/cli.mjs" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", - "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", - "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", - "license": "MIT", - "dependencies": { - "content-type": "^2.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/negotiator/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)", - "optional": true - }, - "node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "dependencies": { + "fast-string-truncated-width": "^3.0.2" } }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", "license": "MIT", - "engines": { - "node": ">=16.20.0" + "dependencies": { + "fast-string-width": "^3.0.2" } }, - "node_modules/pnpm": { - "version": "10.33.2", - "resolved": "https://registry.npmjs.org/pnpm/-/pnpm-10.33.2.tgz", - "integrity": "sha512-qQ+vb+6rca1sblf5Tg/hoS9dzCLNdU20CulZPraj4LaxLjVAIYuzeuCDQEsfLObbKkEh6XmCm0r/lLmfSdoc+A==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, "license": "MIT", - "bin": { - "pnpm": "bin/pnpm.cjs", - "pnpx": "bin/pnpx.cjs" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=18.12" - }, - "funding": { - "url": "https://opencollective.com/pnpm" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT", "optional": true }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "optional": true }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT", - "engines": { - "node": ">=10" - } + "optional": true }, - "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", - "license": "BSD-3-Clause", + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "optional": true, "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "node_modules/quota-axi": { - "version": "0.1.54", - "resolved": "https://registry.npmjs.org/quota-axi/-/quota-axi-0.1.54.tgz", - "integrity": "sha512-g/obfCeyePc4dwq0spouw7fmJwEdrOKtskczPQoJ2YHzXB3ADmnM3/rpEJ2nRDfGrvs1hEyWIz96wrx44RSGxA==", + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", + "optional": true, "dependencies": { - "@toon-format/toon": "2.1.0", - "axi-sdk-js": "^0.1.10", - "proxy-from-env": "2.1.0", - "undici": "6.28.0" - }, - "bin": { - "quota-axi": "dist/bin/quota-axi.js" - }, - "engines": { - "node": ">=22.19" + "immediate": "~3.0.5" } }, - "node_modules/quota-axi/node_modules/@toon-format/toon": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.1.0.tgz", - "integrity": "sha512-JwWptdF5eOA0HaQxbKAzkpQtR4wSWTEfDlEy/y3/4okmOAX1qwnpLZMmtEWr+ncAhTTY1raCKH0kteHhSXnQqg==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)", + "optional": true }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } + "optional": true }, "node_modules/readable-stream": { "version": "2.3.8", @@ -2264,56 +931,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2321,57 +938,6 @@ "license": "MIT", "optional": true }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -2379,105 +945,6 @@ "license": "MIT", "optional": true }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -2501,15 +968,6 @@ "node": ">=18" } }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/streamx": { "version": "2.28.1", "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", @@ -2543,28 +1001,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tasks-axi": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/tasks-axi/-/tasks-axi-0.2.6.tgz", - "integrity": "sha512-LDXSSYB+yVwloxq1XGtZKeWamOf/oD9XFqnohJYlFrqalg5XEne1SpGcNZMQqNzQthsY+iyMvCUeGxOzrL5/9Q==", - "license": "MIT", - "dependencies": { - "@toon-format/toon": "^2.1.0", - "axi-sdk-js": "^0.1.10" - }, - "bin": { - "tasks-axi": "dist/bin/tasks-axi.js" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/tasks-axi/node_modules/@toon-format/toon": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@toon-format/toon/-/toon-2.3.1.tgz", - "integrity": "sha512-sWqEMeAJGMda9qRrfPKrX3C4c33CO9SJuvJzzrcBEn5sbDB+k6pSLkDsfN0rVnLQwR97MV3sDgVbcxQNsq8xVw==", - "license": "MIT" - }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -2583,15 +1019,6 @@ "b4a": "^1.6.4" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, "node_modules/tsx": { "version": "4.23.15", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", @@ -2610,55 +1037,6 @@ "fsevents": "~2.3.3" } }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", - "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2666,51 +1044,6 @@ "license": "MIT", "optional": true }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { "version": "4.6.5", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", @@ -2719,15 +1052,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } } } } diff --git a/tools/npm/package.json b/tools/npm/package.json index 07d4c3f..a35b5d7 100644 --- a/tools/npm/package.json +++ b/tools/npm/package.json @@ -2,16 +2,9 @@ "name": "code-factory-agent-tools", "private": true, "version": "1.0.0", - "description": "Pinned agent and Firstmate CLI tools; no credentials", + "description": "Pinned agent CLI tools; no credentials", "dependencies": { - "@openai/codex": "0.147.0", "acpx": "0.18.0", - "chrome-devtools-axi": "0.1.29", - "chrome-devtools-mcp": "1.9.0", - "gh-axi": "0.1.30", - "lavish-axi": "0.1.52", - "pnpm": "10.33.2", - "quota-axi": "0.1.54", - "tasks-axi": "0.2.6" + "chrome-devtools-mcp": "1.9.0" } } From 654cfec86466b40cd7d7a057512f126025aec938 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 30 Sep 2026 23:51:27 +0000 Subject: [PATCH 02/13] feat(fleet): run lanes through bun, not pnpm The worktree seeder now writes a git-excluded .env.fleet with the node heap cap, and the managed shell and Herdr environment set BUN_OPTIONS=--env-file=.env.fleet under fleet guards, so every bun started in a worktree root (bun run dev, omp) hands NODE_OPTIONS to the node processes it spawns. pnpm is gone from the reaper chain and the docs. --- ansible/group_vars/all.yml | 10 +++-- ansible/tasks/fleet_guards.yml | 4 +- .../flotilla-worktree-env-seed.path.j2 | 2 +- ansible/templates/herdr.service.j2 | 3 +- docs/capacity.md | 2 +- docs/fleet-guards.md | 2 +- fleet/doctor/dev-server-reaper.sh | 6 +-- fleet/doctor/worktree-env-seed.sh | 38 ++++++++++--------- 8 files changed, 37 insertions(+), 30 deletions(-) diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 2d1c9b3..3f2fa87 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -60,9 +60,13 @@ factory_omp_root: "{{ factory_share_dir }}/omp/{{ factory_omp_version }}" factory_chrome_devtools_mcp_path: "{{ factory_npm_root }}/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that -# provides the MCP entrypoint is installed by `--npm`. -factory_managed_shell_env: - CHROME_DEVTOOLS_AXI_MCP_PATH: "{{ factory_chrome_devtools_mcp_path }}" +# provides the MCP entrypoint is installed by `--npm`. With fleet guards, every +# bun loads the worktree's seeded .env.fleet heap cap (a missing file is a +# no-op; see fleet/doctor/worktree-env-seed.sh). +factory_managed_shell_env: >- + {{ {'CHROME_DEVTOOLS_AXI_MCP_PATH': factory_chrome_devtools_mcp_path} + | combine({'BUN_OPTIONS': '--env-file=.env.fleet'} + if (factory_cfg.profiles.fleet_guards | bool) else {}) }} # Contract: compute the unit ExecStart directly, never /usr/local/bin or $PATH. factory_herdr_bin: "{{ factory_tools_root }}/herdr/{{ factory_herdr_version }}/{{ factory_platform }}/herdr" factory_herdr_config_dir: "{{ factory_cfg.home }}/.config/herdr" diff --git a/ansible/tasks/fleet_guards.yml b/ansible/tasks/fleet_guards.yml index 34331a1..78fa688 100644 --- a/ansible/tasks/fleet_guards.yml +++ b/ansible/tasks/fleet_guards.yml @@ -9,8 +9,8 @@ # any Supabase CLI project other than the shared one on creation and alerts # on bare Postgres containers. Allowlist is first-write (operator-owned). # * ~/oss-fleet/doctor/worktree-env-seed.sh - installs the stack's env file -# as .env.local in every swarms worktree, plus a git-excluded .npmrc heap -# cap for pnpm-run scripts; path unit + timer + login. +# as .env.local in every swarms worktree, plus a git-excluded .env.fleet +# heap cap for bun-run scripts; path unit + timer + login. # * ~/oss-fleet/doctor/dev-server-reaper.sh - every 2 min, kills next dev / # tsc trees in worktrees whose lane is done/paused/blocked, agent-less or # idle >= 30 min. Dev servers are throwaway; idle ones filled swap. diff --git a/ansible/templates/flotilla-worktree-env-seed.path.j2 b/ansible/templates/flotilla-worktree-env-seed.path.j2 index af344af..e92d716 100644 --- a/ansible/templates/flotilla-worktree-env-seed.path.j2 +++ b/ansible/templates/flotilla-worktree-env-seed.path.j2 @@ -1,6 +1,6 @@ {{ ansible_managed | comment }} # One PathChanged per worktree pool: a new slot directory in the pool fires the -# seeder before the lane's first `pnpm dev`. Pools: factory.fleet.worktree_pools. +# seeder before the lane's first `bun run dev`. Pools: factory.fleet.worktree_pools. [Unit] Description=Fleet worktree env seed - trigger on swarms-platform pool changes (Code Factory managed) diff --git a/ansible/templates/herdr.service.j2 b/ansible/templates/herdr.service.j2 index 1bab52a..1b326e3 100644 --- a/ansible/templates/herdr.service.j2 +++ b/ansible/templates/herdr.service.j2 @@ -14,8 +14,9 @@ ExecStart={{ factory_herdr_bin }} server WorkingDirectory={{ factory_cfg.workspace }} Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin {% if factory_cfg.profiles.agents | bool %} +# Pinned MCP entrypoint, so AXI never resolves a floating npx package, and +# with fleet guards the bun heap-cap env file. {% for key, value in factory_managed_shell_env | dictsort %} -# Pinned MCP entrypoint, so AXI never resolves a floating npx package. Environment={{ key }}={{ value }} {% endfor %} {% endif %} diff --git a/docs/capacity.md b/docs/capacity.md index 7966249..a08dd72 100644 --- a/docs/capacity.md +++ b/docs/capacity.md @@ -12,7 +12,7 @@ Figures are estimates from a measured fleet host. | omp crewmate (one agent process) | 300-750 MB RSS | measured | | no-mistakes pipeline agent (`omp acp`) while a lane ships | about 450 MB | measured | | `next dev` / `next-server` for a UI lane | 3-4 GB | [fleet guards](fleet-guards.md) | -| `tsc --noEmit` | about 0.6 GB; pnpm scripts capped at a 2048 MB heap | measured; seeded `.npmrc` | +| `tsc --noEmit` | about 0.6 GB; bun-run scripts capped at a 2048 MB heap | measured; seeded `.env.fleet` | | chrome-devtools-axi bridge + `chrome-devtools-mcp` | about 0.3 GB for the MCP child; an idle bridge holds about 2 GB | measured | | Obscura browser tier | about 25 MB idle | [browser ladder](fleet-guards.md#browser-ladder) | | **Light lane** (docs, config, backend: agent + pipeline + tests) | **about 1.35-1.8 GB**; plan 2 GB | sum of the agent, pipeline and `tsc` rows | diff --git a/docs/fleet-guards.md b/docs/fleet-guards.md index 53e42a4..c208c65 100644 --- a/docs/fleet-guards.md +++ b/docs/fleet-guards.md @@ -29,7 +29,7 @@ Sizing per lane count and the full list of pruners are in | `oss-fleet/doctor/dev-server-reaper.sh` | Every 2 minutes: kills `next dev`/`next-server`/`tsc --noEmit` trees in treehouse worktrees whose lane last reported `done:`/`paused:`/`blocked:`/`failed:`, has no agent process, or whose agent transcript is idle >= 30 min (`REAPER_IDLE_MIN`). A dev server is 3-4 GB and restarts in 10 s; idle ones from finished lanes are what filled swap. One `next dev` per branch is inherent - Next compiles the whole app per process - so the fix is lifetime, not sharing. | | `oss-fleet/doctor/storage-guard.sh` | Every 5 minutes: use% of the filesystems holding `/`, `/var/log`, the home and Docker's data root. WARN (85%) alerts once per episode, CRIT (92%) prunes only regenerable Docker data, and a fill rate projecting the disk full within 6 hours alerts even below WARN. See [Storage guard](#storage-guard). | | `oss-fleet/doctor/devtools-bridge-reaper.sh` | Every 10 minutes: stops attached chrome-devtools-axi bridges (`CHROME_DEVTOOLS_AXI_BROWSER_URL` set) whose process tree used no CPU and whose session state files did not change for 60 min (`REAPER_IDLE_MIN`). See [Devtools-bridge reaper](#devtools-bridge-reaper). | -| worktree `.npmrc` (seeded, git-excluded) | `node-options=--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`): pnpm passes it as `NODE_OPTIONS` to every script, so a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. Hidden through the shared `.git/info/exclude`; never written when the repository tracks its own `.npmrc`. | +| worktree `.env.fleet` (seeded, git-excluded) | `NODE_OPTIONS=--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`). The managed shell and Herdr environment set `BUN_OPTIONS=--env-file=.env.fleet`, so every bun started in the worktree root (`bun run dev`, `bun run tsc`, and omp itself) passes the cap to the node processes it spawns, and a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. Bun started outside the worktree root finds no such file and runs uncapped. Hidden through the shared `.git/info/exclude`; never written when the repository tracks its own `.env.fleet`. | | `.local/bin/supabase` | Shim: `status`/`--version` pass through; every lifecycle or schema subcommand is refused with the reason. `npx supabase` bypasses it, which is why the Docker guard exists. | | `.config/systemd/user/flotilla-*.{service,timer,path}` | Login start + 5-minute keeper for the stack; the guard as a restart-always service; the seeder on pool changes, every 2 minutes and at login. | | `/etc/docker/daemon.json` | `init: true` and `live-restore: true` merged in (tasks/docker.yml, any profile with docker). | diff --git a/fleet/doctor/dev-server-reaper.sh b/fleet/doctor/dev-server-reaper.sh index 53a4a7f..ae333c6 100755 --- a/fleet/doctor/dev-server-reaper.sh +++ b/fleet/doctor/dev-server-reaper.sh @@ -93,9 +93,9 @@ agent_alive() { # -> 0 when an agent process has this cwd return 1 } -# Walk up from a target through the wrapper chain (pnpm -> sh -> node) that +# Walk up from a target through the wrapper chain (bun -> sh -> node) that # shares its cwd, stopping before any interactive shell or agent, so the whole -# `pnpm dev` tree dies and does not respawn a child. Never crosses out of the +# `bun run dev` tree dies and does not respawn a child. Never crosses out of the # worktree. tree_root() { # local pid=$1 wt=$2 pp c @@ -103,7 +103,7 @@ tree_root() { # pp=$(ppid_of "$pid"); [ -n "$pp" ] && [ "$pp" -gt 1 ] || break [ "$(cwd_of "$pp")" = "$wt" ] || break c=$(comm_of "$pp") - case "$c" in node|bun|sh|pnpm|npm|npx|MainThread|next-server*) pid=$pp ;; *) break ;; esac + case "$c" in node|bun|sh|npm|npx|MainThread|next-server*) pid=$pp ;; *) break ;; esac done echo "$pid" } diff --git a/fleet/doctor/worktree-env-seed.sh b/fleet/doctor/worktree-env-seed.sh index bac417b..6e92219 100755 --- a/fleet/doctor/worktree-env-seed.sh +++ b/fleet/doctor/worktree-env-seed.sh @@ -32,30 +32,32 @@ seed_one() { install -m 600 "$SRC" "$f" && log "seeded $f" } -# Heap cap for everything pnpm runs in the worktree (next dev, tsc): pnpm reads -# `node-options` from the project .npmrc into NODE_OPTIONS. A runaway compile -# then fails fast with a heap error the agent can see, instead of creeping to -# 4 GB and swapping the host. The file is hidden from git through the shared -# .git/info/exclude (the repository tracks no .npmrc), so no lane ever sees or -# commits it. +# Heap cap for everything bun runs in the worktree (bun run dev, tsc): the +# managed shell and Herdr environment set BUN_OPTIONS=--env-file=.env.fleet, so +# every bun started in the worktree root loads this file and hands NODE_OPTIONS +# to the node processes it spawns. A runaway compile then fails fast with a +# heap error the agent can see, instead of creeping to 4 GB and swapping the +# host. The file is hidden from git through the shared .git/info/exclude, so no +# lane ever sees or commits it. HEAP_MB=${FLEET_NODE_HEAP_MB:-2048} -npmrc_content() { +HEAP_FILE=.env.fleet +heap_content() { printf '%s\n' \ "$MARK (marker; fleet-managed, hidden via .git/info/exclude)" \ - "# Heap cap for pnpm-run scripts (next dev, tsc): fail fast instead of swapping the host." \ - "node-options=--max-old-space-size=$HEAP_MB" + "# Heap cap for bun-run scripts (next dev, tsc): fail fast instead of swapping the host." \ + "NODE_OPTIONS=--max-old-space-size=$HEAP_MB" } -seed_npmrc() { - local wt=$1 f=$1/.npmrc common tmp - if git -C "$wt" ls-files --error-unmatch .npmrc >/dev/null 2>&1; then - return 0 # the repository tracks its own .npmrc; never touch a tracked file +seed_heap_cap() { + local wt=$1 f=$1/$HEAP_FILE common tmp + if git -C "$wt" ls-files --error-unmatch "$HEAP_FILE" >/dev/null 2>&1; then + return 0 # the repository tracks its own file; never touch a tracked file fi common=$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null) || return 0 case "$common" in /*) ;; *) common="$wt/$common" ;; esac - if [ -d "$common/info" ] && ! grep -qxF '.npmrc' "$common/info/exclude" 2>/dev/null; then - printf '%s\n' '.npmrc' >> "$common/info/exclude" && log "excluded .npmrc in $common/info/exclude" + if [ -d "$common/info" ] && ! grep -qxF "$HEAP_FILE" "$common/info/exclude" 2>/dev/null; then + printf '%s\n' "$HEAP_FILE" >> "$common/info/exclude" && log "excluded $HEAP_FILE in $common/info/exclude" fi - tmp=$(mktemp) && npmrc_content > "$tmp" + tmp=$(mktemp) && heap_content > "$tmp" if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; return 0; fi if [ -f "$f" ] && ! grep -qF "$MARK" "$f"; then cp -p "$f" "$f.pre-fleet-$(date -u +%Y%m%dT%H%M%SZ)" @@ -67,7 +69,7 @@ seed_npmrc() { # Worktree pools: ~/.treehouse/-//swarms-platform. A pool slot # directory can exist for a moment before `git worktree add` fills it; give the # checkout up to 2 minutes to appear so a brand-new lane is covered before its -# first `pnpm dev`, not on the next timer tick. +# first `bun run dev`, not on the next timer tick. shopt -s nullglob for slot in "$HOME"/.treehouse/swarms-platform-*/*/; do wt="${slot%/}/swarms-platform" @@ -77,7 +79,7 @@ for slot in "$HOME"/.treehouse/swarms-platform-*/*/; do fi [ -e "$wt/package.json" ] || continue seed_one "$wt" - seed_npmrc "$wt" + seed_heap_cap "$wt" done # Firstmate's primary checkout of the project. for wt in "$HOME"/.treehouse/firstmate-*/*/firstmate/projects/swarms-platform; do From 12fe8041f0c6a8cde7575bc6070387780afae5e0 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 00:13:37 +0000 Subject: [PATCH 03/13] feat: install the latest release of everything, verified by published checksums Nothing the recipe installs is pinned any more. uv, rustup-init, Obscura, acpx and chrome-devtools-mcp join the latest-resolution path; the Rust toolchain follows stable; psutil is the latest PyPI release installed with --require-hashes against PyPI's digests; the Supabase CLI is the registry's latest; the worker image builds FROM ubuntu:latest. Every download is still verified against the checksum its publisher posts for that release. toolchain.lock.json, its schema, tools/npm, maintenance/requirements.txt, the shared-supabase package lock and the obscura config keys are gone. Apply also installs and upgrades the ponytail, i-have-adhd and caveman omp plugins. uv.lock and the SHA-pinned GitHub Actions stay. --- .devcontainer/devcontainer.json | 2 +- .github/workflows/ci.yml | 23 +- CONTRIBUTING.md | 5 +- Dockerfile | 36 +- README.md | 6 +- ansible/group_vars/all.yml | 48 +- ansible/tasks/browser_prune.yml | 32 +- ansible/tasks/fleet-browsers.yml | 57 +- ansible/tasks/fleet_guards.yml | 29 +- ansible/tasks/packages.yml | 4 +- ansible/tasks/preflight.yml | 21 +- ansible/tasks/tools.yml | 5 +- ansible/templates/chrome-autoprune.service.j2 | 4 +- ansible/templates/herdr.service.j2 | 2 +- bootstrap.sh | 6 +- config/default.yml | 2 - containers/factory.container.yml | 2 +- docs/agent-host-move.md | 6 +- docs/architecture.md | 15 +- docs/configuration.md | 7 +- docs/dependencies.md | 45 +- docs/fleet-guards.md | 9 +- docs/omp.md | 2 +- docs/recovery.md | 2 +- docs/security.md | 2 +- fleet/shared-supabase/package-lock.json | 231 ---- fleet/shared-supabase/package.json | 8 - maintenance/requirements.txt | 10 - schemas/factory.schema.json | 13 +- schemas/toolchain.schema.json | 132 -- scripts/factory.py | 18 +- scripts/install_tools.py | 181 +-- tests/container-smoke.sh | 55 +- tests/test_configuration.py | 9 - tests/test_install_tools.py | 22 +- toolchain.lock.json | 50 - tools/npm/package-lock.json | 1057 ----------------- tools/npm/package.json | 10 - 38 files changed, 301 insertions(+), 1867 deletions(-) delete mode 100644 fleet/shared-supabase/package-lock.json delete mode 100644 fleet/shared-supabase/package.json delete mode 100644 maintenance/requirements.txt delete mode 100644 schemas/toolchain.schema.json delete mode 100644 toolchain.lock.json delete mode 100644 tools/npm/package-lock.json delete mode 100644 tools/npm/package.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 91f33eb..021f3d1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,7 +2,7 @@ // 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 already converged by // `./factory apply --config containers/factory.container.yml`. // // Not provided, by design: systemd, user-manager linger, SSH server, Tailscale diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80e00f3..6bab5d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,8 @@ # 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. +# uv and Bun are not pinned: like every ./factory apply, CI installs their +# latest releases. name: ci on: @@ -46,17 +45,10 @@ 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 + - name: Install latest uv uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: - version: ${{ steps.uv.outputs.version }} + version: latest enable-cache: true - name: Sync locked dependencies @@ -98,7 +90,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' @@ -117,8 +109,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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 950b724..a8e32fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,6 @@ Code Factory is an Ansible playbook with a Python CLI wrapper (`scripts/factory. - `ansible/templates/*.j2` — systemd unit templates - `scripts/factory.py` — CLI (`init`, `validate`, `plan`, `apply`, `doctor`) - `scripts/install_tools.py` — checksum-verified tool installer (idempotent) -- `toolchain.lock.json` — the pinned tools (uv, rustup-init, Rust): version, URL, sha256 - `fleet/` — runtime scripts deployed to `~/oss-fleet/` on the target host - `config/` — per-tool config templates deployed to Firstmate homes @@ -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. -Only uv, rustup-init and the Rust toolchain are pinned in `toolchain.lock.json`. Every other tool tracks its latest release, resolved once per apply: native assets are verified against the SHA-256 their publisher lists for that release and refused without one, npm tools install the exact resolved version. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. +Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts. Native assets are refused without a published SHA-256, npm tools install the exact resolved version, and the Rust toolchain follows `stable`. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. Only the repository's own Python environment (`uv.lock`) and the GitHub Actions in CI stay locked. No unconditional restarts, daemon-reloads, or bare commands. @@ -48,7 +47,7 @@ uv run ansible-playbook -i ansible/inventory.yml ansible/site.yml --syntax-check ## Adding a new tool -1. Pin it in `toolchain.lock.json` with the correct `format` (`file`, `tar` or `zip`), sha256, URL, and `binaries` map, and allow its name in `schemas/toolchain.schema.json`. A tool that tracks its latest release goes in `GITHUB_LATEST` or `NPM_LATEST` in `scripts/install_tools.py` instead. +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. Add the tool name to `factory_core_tools` in `group_vars/all.yml` (or a profile-gated list). 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). diff --git a/Dockerfile b/Dockerfile index 4cd516e..04dbb9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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) @@ -26,13 +20,12 @@ # # 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. -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 @@ -44,7 +37,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; \ @@ -144,7 +137,7 @@ 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). +# Latest uv bootstrap + locked Python dependencies (no provisioning yet). RUN set -eux; ./bootstrap.sh # Schema validation through the repository's own validator. @@ -155,11 +148,11 @@ 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 +# The real convergence run. `apply` installs the latest, checksum-verified +# 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-release +# lookups (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. RUN --mount=type=secret,id=github_token,env=GITHUB_TOKEN \ @@ -171,8 +164,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"] diff --git a/README.md b/README.md index 7eb9e83..934d14d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Turn a fresh Ubuntu machine into a reproducible AI-agent coding host: Herdr, Fir ```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"] @@ -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 @@ -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 tool, package, and image, pinned or latest, 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 | diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 3f2fa87..baa7235 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -20,14 +20,11 @@ factory_cfg: "{{ factory_defaults | combine(factory | default({}, true), recursi # the checkout. code_factory_repo: "{{ playbook_dir | dirname }}" factory_installer: "{{ code_factory_repo }}/scripts/install_tools.py" -factory_lock_file: "{{ code_factory_repo }}/toolchain.lock.json" factory_pruner_source: "{{ code_factory_repo }}/maintenance/chrome-autoprune.py" -factory_pruner_requirements: "{{ code_factory_repo }}/maintenance/requirements.txt" -# Every tool except uv and the Rust toolchain tracks its latest release: -# tasks/preflight.yml resolves them once per run into factory_latest, and that -# is what the run installs and tasks/verify.yml asserts for herdr and omp. -# uv and Rust come from the reviewed lock. +# Nothing is pinned: tasks/preflight.yml resolves every tool's latest release +# once per run into factory_latest, and that is what the run installs and +# tasks/verify.yml asserts for herdr and omp. factory_herdr_version: "{{ factory_latest.herdr.version }}" factory_omp_version: "{{ factory_latest.omp }}" @@ -51,13 +48,12 @@ factory_allow_unsupported_os: false factory_local_bin: "{{ factory_cfg.home }}/.local/bin" factory_share_dir: "{{ factory_cfg.home }}/.local/share/code-factory" factory_tools_root: "{{ factory_share_dir }}/tools" -# Locked npm set installed by `install_tools.py --npm` (agents profile). -factory_npm_root: "{{ factory_share_dir }}/npm" factory_omp_root: "{{ factory_share_dir }}/omp/{{ factory_omp_version }}" -# AXI must launch the pinned chrome-devtools MCP build instead of resolving -# `npx ...@latest` at runtime. Exported to the managed shell environment and -# to the Herdr service environment. -factory_chrome_devtools_mcp_path: "{{ factory_npm_root }}/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" +# AXI must launch the chrome-devtools MCP build this run installed (the latest +# release, under its versioned prefix) instead of resolving `npx ...@latest` +# at runtime. Exported to the managed shell environment and to the Herdr +# service environment. +factory_chrome_devtools_mcp_path: "{{ factory_share_dir }}/chrome-devtools-mcp/{{ factory_latest['chrome-devtools-mcp'] }}/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that # provides the MCP entrypoint is installed by `--npm`. With fleet guards, every @@ -82,8 +78,9 @@ factory_vnc_chrome_profile: "{{ factory_cfg.home }}/.vnc-chrome-profile" # psutil.Process.net_connections(), which exists from psutil 6 onwards, while # Ubuntu 24.04 ships python3-psutil 5.9. Instead of shimming the deprecated # API or pip-installing into the distribution interpreter, the pruner runs on -# an account-owned virtualenv built by the managed uv and filled from -# maintenance/requirements.txt with --require-hashes. The unit references only +# an account-owned virtualenv built by the managed uv and filled with the latest +# psutil, --require-hashes against the digests PyPI publishes for that release +# (factory_latest.psutil). The unit references only # paths under the account's home, so the timer keeps working after the # repository checkout is gone. factory_uv_bin: "{{ factory_local_bin }}/uv" @@ -91,12 +88,15 @@ factory_pruner_script: "{{ factory_local_bin }}/chrome-autoprune.py" factory_pruner_venv: "{{ factory_share_dir }}/pruner-venv" factory_pruner_python: "{{ factory_pruner_venv }}/bin/python" # Written after a successful install; carries the digest of the requirements -# file that produced the runtime, so a changed pin reinstalls and an unchanged -# pin is a no-op on the second apply. +# that produced the runtime, so a new psutil release reinstalls and an +# unchanged one is a no-op on the second apply. factory_pruner_stamp: "{{ factory_pruner_venv }}/.code-factory-runtime.json" -factory_pruner_requirements_text: "{{ lookup('ansible.builtin.file', factory_pruner_requirements) }}" +factory_pruner_requirements: "{{ factory_share_dir }}/pruner-requirements.txt" +factory_pruner_psutil_version: "{{ factory_latest.psutil.version }}" +factory_pruner_requirements_text: >- + psutil=={{ factory_pruner_psutil_version }} + {{ factory_latest.psutil.sha256 | map('regex_replace', '^', '--hash=sha256:') | join(' ') }} factory_pruner_requirements_digest: "{{ factory_pruner_requirements_text | hash('sha256') }}" -factory_pruner_psutil_version: '{{ factory_pruner_requirements_text | regex_findall("(?m)^psutil==([A-Za-z0-9][A-Za-z0-9._-]*)") | first | default("", true) }}' # The base interpreter of that virtualenv. Pinned to the distribution # interpreter so no managed Python is downloaded behind the operator's back. factory_pruner_base_python: /usr/bin/python3 @@ -157,7 +157,7 @@ factory_development_packages: - python3-dev - cmake - ripgrep -# No python3-psutil here on purpose: the pruner runs on its own hash-pinned +# No python3-psutil here on purpose: the pruner runs on its own hash-verified # virtualenv (factory_pruner_venv), not on the distribution interpreter. factory_docker_packages: - docker.io @@ -281,11 +281,13 @@ factory_shared_supabase_cli: "{{ factory_shared_supabase_dir }}/node_modules/.bi factory_shared_supabase_project: "{{ factory_cfg.fleet.supabase_project_id }}" factory_shared_supabase_db_volume: "supabase_db_{{ factory_shared_supabase_project }}" factory_shared_supabase_storage_volume: "supabase_storage_{{ factory_shared_supabase_project }}" -# Pinned by fleet/shared-supabase/package-lock.json; read here for the version check. -factory_shared_supabase_cli_version: "{{ (lookup('ansible.builtin.file', factory_fleet_sources ~ '/shared-supabase/package.json') | from_json).dependencies.supabase }}" +# The npm registry's latest supabase, resolved once per run (factory_latest). +factory_shared_supabase_cli_version: "{{ factory_latest.supabase }}" factory_fleet_browsers_dir: "{{ factory_fleet_dir }}/browsers" -factory_browser_obscura_url: "https://github.com/h4ckf0r0day/obscura/releases/download/v{{ factory_cfg.browsers.obscura_version }}/obscura-x86_64-linux.tar.gz" -factory_browser_obscura_checksum: "sha256:{{ factory_cfg.browsers.obscura_sha256 }}" +# The latest Obscura release for this platform, with the SHA-256 GitHub +# publishes for the asset; each release extracts into its own directory. +factory_browser_obscura: "{{ factory_latest.obscura.assets[factory_platform] }}" +factory_browser_obscura_dir: "{{ factory_fleet_browsers_dir }}/obscura-{{ factory_latest.obscura.version }}" # Storage guard (fleet/doctor/storage-guard.sh, every 5 min). Use% of the # filesystems holding /, /var/log, the home and Docker's data root: WARN alerts diff --git a/ansible/tasks/browser_prune.yml b/ansible/tasks/browser_prune.yml index cdf9b24..cb5aa85 100644 --- a/ansible/tasks/browser_prune.yml +++ b/ansible/tasks/browser_prune.yml @@ -8,8 +8,9 @@ # # Runtime: the pruner calls psutil.Process.net_connections(), which needs # psutil >= 6, while Ubuntu 24.04 ships python3-psutil 5.9. It therefore runs -# on an account-owned virtualenv created by the managed uv and filled from -# maintenance/requirements.txt with --require-hashes. The distribution +# on an account-owned virtualenv created by the managed uv and filled with the +# latest psutil, --require-hashes against the digests PyPI publishes for that +# release (factory_latest.psutil, resolved in tasks/preflight.yml). The distribution # interpreter is never pip-installed into, and the unit only references paths # under the account's home, so the timer survives deletion of this checkout. @@ -17,8 +18,8 @@ ansible.builtin.debug: msg: >- check mode: would provide psutil {{ factory_pruner_psutil_version }} in - {{ factory_pruner_venv }} via {{ factory_uv_bin }} and - {{ factory_pruner_requirements | relpath(code_factory_repo) }}. + {{ factory_pruner_venv }} via {{ factory_uv_bin }}, verified against the + digests PyPI publishes. when: ansible_check_mode - name: Build the private pruner runtime @@ -38,7 +39,7 @@ that: - factory_prune_uv.stat.exists | default(false) fail_msg: >- - {{ factory_uv_bin }} is missing, so the hash-pinned pruner runtime + {{ factory_uv_bin }} is missing, so the hash-verified pruner runtime cannot be built. uv is a core tool of the installer (factory_core_tools); fix tasks/tools.yml output before re-running. Provisioning refuses to fall back to the distribution psutil, which @@ -66,7 +67,7 @@ failed_when: false changed_when: false - - name: Decide whether the runtime already matches the pinned requirements + - name: Decide whether the runtime already matches the resolved psutil ansible.builtin.set_fact: factory_prune_runtime_current: >- {{ (factory_prune_installed.rc | default(1)) == 0 @@ -84,7 +85,17 @@ environment: "{{ factory_user_env | combine(factory_pruner_uv_env) }}" notify: restart chrome autoprune timer - - name: Install the hash-pinned pruner requirements + - name: Write the resolved pruner requirements + ansible.builtin.copy: + dest: "{{ factory_pruner_requirements }}" + content: "{{ factory_pruner_requirements_text }}\n" + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0644" + become: true + when: not (factory_prune_runtime_current | bool) + + - name: Install the hash-verified pruner requirements ansible.builtin.command: argv: - "{{ factory_uv_bin }}" @@ -103,7 +114,7 @@ when: not (factory_prune_runtime_current | bool) notify: restart chrome autoprune timer - - name: Confirm the runtime provides the pinned psutil + - name: Confirm the runtime provides the resolved psutil ansible.builtin.command: argv: ["{{ factory_pruner_python }}", -c, "import psutil; print(psutil.__version__)"] become: "{{ factory_become_target | bool }}" @@ -112,14 +123,13 @@ register: factory_prune_verified changed_when: false - - name: Assert the runtime psutil matches the pin + - name: Assert the runtime psutil is the resolved release ansible.builtin.assert: that: - (factory_prune_verified.stdout | trim) == factory_pruner_psutil_version fail_msg: >- {{ factory_pruner_python }} reports psutil - "{{ factory_prune_verified.stdout | trim }}" but - {{ factory_pruner_requirements | relpath(code_factory_repo) }} pins + "{{ factory_prune_verified.stdout | trim }}" but this run resolved {{ factory_pruner_psutil_version }}. The pruner needs Process.net_connections() (psutil >= 6); it is not run against an unexpected build. diff --git a/ansible/tasks/fleet-browsers.yml b/ansible/tasks/fleet-browsers.yml index ce782e3..dee582f 100644 --- a/ansible/tasks/fleet-browsers.yml +++ b/ansible/tasks/fleet-browsers.yml @@ -1,7 +1,7 @@ --- # Fleet browsers: obscura -> chrome -> vnc tier ladder, shared session jar. # docs/fleet-guards.md owns the design; fleet/browsers/fleet-browser owns the -# runtime. Only the obscura tier needs provisioning (pinned binary + unit); +# runtime. Only the obscura tier needs provisioning (latest binary + unit); # chrome and vnc are started on demand by the operator or the fleet-browser # CLI and gc'd after idle_timeout_min. @@ -31,33 +31,41 @@ - { src: browsers/cookie-sync.ts, dest: "{{ factory_fleet_browsers_dir }}/cookie-sync.ts", mode: "0644" } - { src: browsers/env.sh, dest: "{{ factory_fleet_browsers_dir }}/env.sh", mode: "0644" } -- name: Stat the obscura binary +- name: Stat the resolved Obscura release ansible.builtin.stat: - path: "{{ factory_fleet_browsers_dir }}/obscura" - get_checksum: true - checksum_algorithm: sha256 + path: "{{ factory_browser_obscura_dir }}/obscura" + get_checksum: false register: factory_obscura_stat changed_when: false check_mode: false -- name: Download and extract the pinned Obscura binary - when: >- - not factory_obscura_stat.stat.exists - or factory_obscura_stat.stat.checksum != factory_cfg.browsers.obscura_sha256 +# Each release extracts into its own directory, so an unchanged release is a +# no-op and a new one never overwrites binaries a running tier holds open. +- name: Download and extract the latest Obscura release + when: not factory_obscura_stat.stat.exists block: + - name: Create the Obscura release directory + ansible.builtin.file: + path: "{{ factory_browser_obscura_dir }}" + state: directory + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0755" + become: true + - name: Fetch the obscura tarball ansible.builtin.get_url: - url: "{{ factory_browser_obscura_url }}" - dest: "{{ factory_fleet_browsers_dir }}/obscura.tar.gz" - checksum: "{{ factory_browser_obscura_checksum }}" + url: "{{ factory_browser_obscura.url }}" + dest: "{{ factory_browser_obscura_dir }}.tar.gz" + checksum: "sha256:{{ factory_browser_obscura.sha256 }}" mode: "0600" owner: "{{ factory_cfg.user }}" become: true - name: Extract obscura binaries ansible.builtin.unarchive: - src: "{{ factory_fleet_browsers_dir }}/obscura.tar.gz" - dest: "{{ factory_fleet_browsers_dir }}" + src: "{{ factory_browser_obscura_dir }}.tar.gz" + dest: "{{ factory_browser_obscura_dir }}" remote_src: true owner: "{{ factory_cfg.user }}" group: "{{ factory_group }}" @@ -66,18 +74,21 @@ - name: Clean up the obscura tarball ansible.builtin.file: - path: "{{ factory_fleet_browsers_dir }}/obscura.tar.gz" + path: "{{ factory_browser_obscura_dir }}.tar.gz" state: absent become: true -- name: Assert the obscura binary matches the lock - ansible.builtin.assert: - that: - - factory_obscura_stat.stat.exists | default(false) - fail_msg: >- - {{ factory_fleet_browsers_dir }}/obscura absent after extract; check - {{ factory_browser_obscura_url }}. - quiet: true +- name: Point the fleet browser at the resolved Obscura release + ansible.builtin.file: + src: "{{ factory_browser_obscura_dir }}/{{ item }}" + dest: "{{ factory_fleet_browsers_dir }}/{{ item }}" + state: link + force: true + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + become: true + loop: [obscura, obscura-worker] + notify: restart fleet browser obscura when: not ansible_check_mode - name: Inject the shared-browser env into shell profiles diff --git a/ansible/tasks/fleet_guards.yml b/ansible/tasks/fleet_guards.yml index 78fa688..d0befc5 100644 --- a/ansible/tasks/fleet_guards.yml +++ b/ansible/tasks/fleet_guards.yml @@ -2,8 +2,8 @@ # Fleet guards: ONE shared local Supabase stack for every swarms-platform lane, # enforced at every layer that a crewmate can reach (docs/fleet-guards.md). # -# * ~/oss-fleet/shared-supabase - the stack's project dir: pinned Supabase -# CLI (npm ci from fleet/shared-supabase/package-lock.json), config.toml, +# * ~/oss-fleet/shared-supabase - the stack's project dir: the latest +# Supabase CLI from the npm registry, config.toml, # check.sh keeper (idempotent start + DDL guard + env template), guard.sql. # * ~/oss-fleet/doctor/docker-guard.sh - `docker events` watcher that removes # any Supabase CLI project other than the shared one on creation and alerts @@ -73,8 +73,6 @@ - { src: shared-supabase/check.sh, dest: "{{ factory_shared_supabase_dir }}/check.sh", mode: "0755" } - { src: shared-supabase/guard.sql, dest: "{{ factory_shared_supabase_dir }}/guard.sql", mode: "0644" } - { src: shared-supabase/README.md, dest: "{{ factory_shared_supabase_dir }}/README.md", mode: "0644" } - - { src: shared-supabase/package.json, dest: "{{ factory_shared_supabase_dir }}/package.json", mode: "0644" } - - { src: shared-supabase/package-lock.json, dest: "{{ factory_shared_supabase_dir }}/package-lock.json", mode: "0644" } - { src: bin/supabase, dest: "{{ factory_local_bin }}/supabase", mode: "0755" } loop_control: label: "{{ item.dest }}" @@ -131,7 +129,7 @@ quiet: true when: not ansible_check_mode -# --- pinned Supabase CLI ------------------------------------------------------ +# --- latest Supabase CLI ------------------------------------------------------ - name: Read the installed Supabase CLI version ansible.builtin.command: @@ -147,15 +145,24 @@ - name: Report the CLI install that check mode cannot perform ansible.builtin.debug: msg: >- - check mode: would run `npm ci` in {{ factory_shared_supabase_dir }} for - supabase {{ factory_shared_supabase_cli_version }}. + check mode: would run `npm install` in {{ factory_shared_supabase_dir }} + for supabase {{ factory_shared_supabase_cli_version }}. when: - ansible_check_mode - (factory_fleet_cli_version.stdout | default('') | trim) != factory_shared_supabase_cli_version -- name: Install the pinned Supabase CLI from the lock +# npm checks the integrity the registry records for the resolved version. +- name: Install the latest Supabase CLI ansible.builtin.command: - argv: [npm, ci, --no-fund, --no-audit, --loglevel=error] + argv: + - npm + - install + - --prefix + - "{{ factory_shared_supabase_dir }}" + - --no-fund + - --no-audit + - --loglevel=error + - "supabase@{{ factory_shared_supabase_cli_version }}" chdir: "{{ factory_shared_supabase_dir }}" become: "{{ factory_become_target | bool }}" become_user: "{{ factory_cfg.user }}" @@ -165,7 +172,7 @@ - not ansible_check_mode - (factory_fleet_cli_version.stdout | default('') | trim) != factory_shared_supabase_cli_version -- name: Confirm the Supabase CLI matches the lock +- name: Confirm the Supabase CLI is the resolved release ansible.builtin.command: argv: ["{{ factory_shared_supabase_cli }}", --version] become: "{{ factory_become_target | bool }}" @@ -182,7 +189,7 @@ fail_msg: >- {{ factory_shared_supabase_cli }} reports "{{ factory_fleet_cli_verified.stdout | default('') | trim }}" but - fleet/shared-supabase/package.json pins {{ factory_shared_supabase_cli_version }}. + this run resolved {{ factory_shared_supabase_cli_version }}. quiet: true when: not ansible_check_mode diff --git a/ansible/tasks/packages.yml b/ansible/tasks/packages.yml index 6a2952d..282a61b 100644 --- a/ansible/tasks/packages.yml +++ b/ansible/tasks/packages.yml @@ -1,5 +1,5 @@ --- -# Ubuntu prerequisites. Only distribution packages here; every pinned tool +# Ubuntu prerequisites. Only distribution packages here; every other tool # comes from the repository installer (tasks/tools.yml). # # The cache refresh is marked unchanged on purpose: refreshing package metadata @@ -42,6 +42,6 @@ - not ansible_check_mode or factory_apt_indexes.matched > 0 # The browser pruner needs psutil >= 6 for Process.net_connections(); Ubuntu -# 24.04 ships 5.9. It therefore gets a hash-pinned private virtualenv in +# 24.04 ships 5.9. It therefore gets a hash-verified private virtualenv in # tasks/browser_prune.yml instead of a distribution package, and nothing here # installs into /usr/bin/python3's site-packages. diff --git a/ansible/tasks/preflight.yml b/ansible/tasks/preflight.yml index 30f50ce..0d48a6e 100644 --- a/ansible/tasks/preflight.yml +++ b/ansible/tasks/preflight.yml @@ -47,13 +47,13 @@ - name: Build the list of Main-owned inputs this run requires ansible.builtin.set_fact: factory_required_inputs: >- - {{ [factory_installer, factory_lock_file, factory_herdr_spaces_source] + {{ [factory_installer, factory_herdr_spaces_source] + (factory_agent_config_files | map(attribute='src') | map('regex_replace', '^', code_factory_repo ~ '/') | list if (factory_cfg.profiles.agents | bool) else []) - + ([factory_pruner_source, factory_pruner_requirements] + + ([factory_pruner_source] if ((factory_cfg.profiles.agents | bool) and (factory_cfg.browser_prune.enabled | bool)) else []) + (factory_firstmate_config_names @@ -89,7 +89,7 @@ # asset. - name: Resolve the latest tool releases ansible.builtin.command: - argv: [python3, "{{ factory_installer }}", --lock, "{{ factory_lock_file }}", --home, "{{ factory_cfg.home }}", --resolve] + argv: [python3, "{{ factory_installer }}", --home, "{{ factory_cfg.home }}", --resolve] register: factory_latest_run changed_when: false check_mode: false @@ -98,21 +98,6 @@ ansible.builtin.set_fact: factory_latest: "{{ factory_latest_run.stdout | from_json }}" -- name: Assert the pruner requirements pin an exact psutil release - ansible.builtin.assert: - that: - - factory_pruner_psutil_version | length > 0 - - factory_pruner_requirements_text is search('--hash=sha256:') - fail_msg: >- - {{ factory_pruner_requirements }} must pin psutil with `psutil==` - and carry --hash=sha256: entries. The pruner runtime is installed with - `uv pip install --require-hashes`; an unpinned or unhashed requirement is - refused instead of being installed loosely. - quiet: true - when: - - factory_cfg.profiles.agents | bool - - factory_cfg.browser_prune.enabled | bool - - name: Show the resolved provisioning plan ansible.builtin.debug: msg: diff --git a/ansible/tasks/tools.yml b/ansible/tasks/tools.yml index e5dc2b2..52f01ef 100644 --- a/ansible/tasks/tools.yml +++ b/ansible/tasks/tools.yml @@ -1,8 +1,8 @@ --- # Toolchain, installed by the repository installer as the target user: the -# lock's pins plus the latest releases tasks/preflight.yml resolved. +# latest releases tasks/preflight.yml resolved. # -# Contract: python3 /scripts/install_tools.py --lock /toolchain.lock.json +# Contract: python3 /scripts/install_tools.py # --home --tools herdr,node,bun,uv [--npm] [--development] # --resolved # The installer is idempotent and prints a final JSON object @@ -13,7 +13,6 @@ ansible.builtin.set_fact: factory_installer_argv: >- {{ ['python3', factory_installer, - '--lock', factory_lock_file, '--home', factory_cfg.home, '--tools', factory_core_tools | join(','), '--resolved', factory_latest | to_json] diff --git a/ansible/templates/chrome-autoprune.service.j2 b/ansible/templates/chrome-autoprune.service.j2 index 0aab069..b32aa1c 100644 --- a/ansible/templates/chrome-autoprune.service.j2 +++ b/ansible/templates/chrome-autoprune.service.j2 @@ -2,7 +2,7 @@ # Triggered by chrome-autoprune.timer; not enabled on its own. # # The interpreter is the account's private pruner runtime, which carries the -# hash-pinned psutil {{ factory_pruner_psutil_version }} the pruner needs for +# hash-verified psutil {{ factory_pruner_psutil_version }} the pruner needs for # Process.net_connections(). Only paths under the account's home are # referenced, so this unit keeps working without the repository checkout. @@ -13,7 +13,7 @@ ConditionPathExists={{ factory_pruner_python }} [Service] Type=oneshot -# The managed bin directory comes first so the pruner sees the pinned Node and +# The managed bin directory comes first so the pruner sees the managed Node and # AXI command wrappers the installer linked there. Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ExecStart={{ factory_pruner_python }} {{ factory_pruner_script }} {{ factory_prune_exec_args | join(' ') }} diff --git a/ansible/templates/herdr.service.j2 b/ansible/templates/herdr.service.j2 index 1b326e3..2bc5203 100644 --- a/ansible/templates/herdr.service.j2 +++ b/ansible/templates/herdr.service.j2 @@ -14,7 +14,7 @@ ExecStart={{ factory_herdr_bin }} server WorkingDirectory={{ factory_cfg.workspace }} Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin {% if factory_cfg.profiles.agents | bool %} -# Pinned MCP entrypoint, so AXI never resolves a floating npx package, and +# The installed MCP entrypoint, so AXI never resolves a floating npx package, and # with fleet guards the bun heap-cap env file. {% for key, value in factory_managed_shell_env | dictsort %} Environment={{ key }}={{ value }} diff --git a/bootstrap.sh b/bootstrap.sh index 8287350..6b65234 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -5,10 +5,8 @@ ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" command -v python3 >/dev/null 2>&1 || { echo 'Install Python 3.12+ (Ubuntu 24.04/26.04) first.' >&2; exit 1; } python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) else "Python 3.12+ required")' export PATH="$HOME/.local/bin:$PATH" -WANTED_UV="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["tools"]["uv"]["version"])' "$ROOT/toolchain.lock.json")" -if ! command -v uv >/dev/null 2>&1 || [[ "$(uv --version)" != "uv $WANTED_UV"* ]]; then - python3 "$ROOT/scripts/install_tools.py" --lock "$ROOT/toolchain.lock.json" --home "$HOME" --tools uv -fi +# The latest uv release, checksum-verified; a no-op when it is already installed. +python3 "$ROOT/scripts/install_tools.py" --home "$HOME" --tools uv >/dev/null uv sync --project "$ROOT" --locked if (($#)); then exec "$ROOT/factory" "$@" diff --git a/config/default.yml b/config/default.yml index c212d34..60ac42b 100644 --- a/config/default.yml +++ b/config/default.yml @@ -79,8 +79,6 @@ factory: worktree_pools: - .treehouse/swarms-platform-bcc5ea browsers: - obscura_version: "0.2.2" - obscura_sha256: c1b4548e36549a0228c39c1cc842df425bc7253af2b0a56bd2a538d8ff7e3406 idle_timeout_min: 30 firstmate: # Upstream Firstmate's main, fetched and fast-forwarded on every apply so a diff --git a/containers/factory.container.yml b/containers/factory.container.yml index a18e046..fe78e09 100644 --- a/containers/factory.container.yml +++ b/containers/factory.container.yml @@ -21,7 +21,7 @@ # process groups; it is native-only. # # profiles.agents and profiles.development stay true: the image installs the real -# checksum-pinned agent and development toolchain through scripts/install_tools.py. +# latest, checksum-verified agent and development toolchain through scripts/install_tools.py. schema_version: 1 factory: user: coder diff --git a/docs/agent-host-move.md b/docs/agent-host-move.md index df32206..8ca025c 100644 --- a/docs/agent-host-move.md +++ b/docs/agent-host-move.md @@ -37,7 +37,7 @@ Services that hold state other hosts share, such as a model relay, a monitoring | Each home's untracked `.omp/mcp.json` and `.omp/rules/` | `rsync -a` them with the home's `data/` and `config/` when they hold no credentials. Then rewrite the home path in `.omp/mcp.json` with the `sed` of the crontab row. | | no-mistakes | Apply seeds `~/.no-mistakes/config.yaml` with the gate agent. Merge any other settings from the old host's file by hand, without host-specific paths such as `acpx_path`. Then run `no-mistakes init` in every gated clone on the new host that has no `no-mistakes` remote yet (provisioning initializes the clones it makes). List the gated clones on the old host with `python3 -c "import sqlite3; [print(r[0]) for r in sqlite3.connect('file:$HOME/.no-mistakes/state.sqlite?mode=ro', uri=True).execute('select working_path from repos')]"`, and keep only paths that still exist under a home's `projects/`. Do not copy `state.sqlite` or `repos/`. | | omp settings (`~/.omp/agent/config.yml`) | Apply writes `config/omp.yml` there only when the file is absent, so the new host starts from the seed. Diff the old host's file against the new one and merge by hand ([Updating an existing host](omp.md#updating-an-existing-host)). Review host-specific keys such as `browser.cdpUrl` before merging them. | -| omp plugins and skill roots | Run `omp plugin marketplace add` and `omp plugin install` for each plugin the old host lists in `omp plugin list`, then confirm with `omp plugin list` on the new host. `rsync -a` the skill roots `~/.omp/agent/skills`, `~/.omp/agent/managed-skills` and `~/.agents/skills`. | +| omp plugins and skill roots | Apply installs ponytail, i-have-adhd and caveman and upgrades them to their latest release. For any other plugin the old host lists in `omp plugin list`, run `omp plugin marketplace add` and `omp plugin install`, then confirm with `omp plugin list` on the new host. `rsync -a` the skill roots `~/.omp/agent/skills`, `~/.omp/agent/managed-skills` and `~/.agents/skills`. | | systemd user units | Apply writes its own. After apply, list what the new host lacks: `comm -23 <(ssh 'ls ~/.config/systemd/user' \| sort) <(ls ~/.config/systemd/user \| sort)`. Move only agent-side units from that list. Never move units for services that stay on the old host, and never copy a unit or drop-in that holds an inline credential ([Never export](security.md#never-export)). | | User crontab | Crontab entries and the scripts they run often name a home's path. After every sync of a home's `data/` to a host where its path differs, run `sed -i 's###g'` on those scripts there. [Cutover](#cutover) comments the entries out on the old host at step 1, adds them with the new path on the new host at step 6, and deletes them on the old host at step 8. | | Hand-installed tools in `~/.local/bin` | After apply, list what the new host lacks: `comm -23 <(ssh 'ls ~/.local/bin' \| sort) <(ls ~/.local/bin \| sort)`. Reinstall each agent-side tool from its source and skip backups. | @@ -62,8 +62,8 @@ CI runs the `agent-gate` check in `tests/container-smoke.sh` for the gate agent, | The VNC browser tier finds a Chrome binary | `test -x /usr/bin/google-chrome` | Exit `0`. | | Crew advisor calls skip the server-side fallback | `grep -A2 '^providers:' ~/Dev/firstmate/config/omp-crew-overlay.yml` | `serverSideFallback: false` under `anthropic:`. | | ponytail-review runs | `git -C diff HEAD~1 \| ponytail-review --stdin; echo $?` | Exit `0` or `2`, never `1`. | -| no-mistakes gate agent | `no-mistakes --version`, `no-mistakes doctor`, `jq -r .agents.omp.command ~/.acpx/config.json`, `grep -c '^acp_registry_overrides' ~/.no-mistakes/config.yaml` | The version in `toolchain.lock.json`, doctor reports `acp:omp` runnable, `omp acp`, `0`. | -| AXI tool floors | `quota-axi --version; tasks-axi --version` | At least the floors in [Dependencies](dependencies.md#agent-clis). | +| no-mistakes gate agent | `no-mistakes --version`, `no-mistakes doctor`, `jq -r .agents.omp.command ~/.acpx/config.json`, `grep -c '^acp_registry_overrides' ~/.no-mistakes/config.yaml` | The version in `~/.local/share/code-factory/resolved.json`, doctor reports `acp:omp` runnable, `omp acp`, `0`. | +| AXI tool floors | `quota-axi --version; tasks-axi --version` | At least the floors in [Dependencies](dependencies.md#latest-releases). | ## Cutover diff --git a/docs/architecture.md b/docs/architecture.md index 42c54a0..6e9756c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,7 +15,7 @@ The source machine already uses Ubuntu packages, user-level systemd services, ho | OpenTofu | Cloud instances, networks, DNS, resource lifecycle | Add when a provider/resource contract is chosen; no pretend provider configuration is shipped | | cloud-init | Initial VM prerequisites before configuration management | Small vendor-neutral bootstrap input only | -This is repeatable configuration, not a bit-identical OS image. Ubuntu packages receive distribution security updates. uv and the Rust toolchain are locked to exact versions; every other native and agent tool tracks its latest release, verified against its publisher's checksum, so a later rebuild can install newer versions. Rebuilding an environment does not recreate authenticated accounts, databases, or running processes. +This is repeatable configuration, not a bit-identical OS image. Ubuntu packages receive distribution security updates. Everything the recipe installs is latest, verified by published checksums, so a later rebuild installs newer versions. Rebuilding an environment does not recreate authenticated accounts, databases, or running processes. ## Host and container boundary @@ -53,9 +53,9 @@ Host sizing and every auto pruner are listed in [Capacity and pruners](capacity. ## Reproducibility policy -1. Update the pinned native tools (uv, rustup-init) and both architecture hashes together in `toolchain.lock.json`, and the Rust toolchain version there. Every other native tool (herdr, node, bun, gh, no-mistakes, treehouse) tracks its latest release: each apply resolves it once and verifies the asset against the SHA-256 its publisher lists for that exact release (the GitHub release-asset digest, or Node's `SHASUMS256.txt`), refusing a release without one. Node is the newest release in the nodejs.org index, not the LTS line. -2. Update exact npm dependencies (acpx, chrome-devtools-mcp) and regenerate `tools/npm/package-lock.json` together. Do not copy a live global package directory. The exceptions are omp and the AXI tools (chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, tasks-axi): each apply resolves them to the registry's latest version and installs exactly that, each in its own versioned prefix. -3. Change Python dependencies with `uv lock` and commit the lock. +1. Everything latest, verified by published checksums. Each apply resolves every tool's newest release once (`scripts/install_tools.py --resolve`), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The Rust toolchain follows `stable`, omp plugins are upgraded in place, and the worker image builds `FROM ubuntu:latest`. +2. Do not copy a live global package directory. +3. Two locks stay, because they are this repository's own development environment rather than installed tools: change Python dependencies with `uv lock` and commit the lock, and keep GitHub Actions pinned to commit SHAs. 4. Keep machine differences in ignored `.local/host.yml`; schema validation precedes provisioning. 5. Do not force, stash, reset, or overwrite a modified Firstmate checkout or an unmanaged command. Resolve that conflict explicitly. 6. Keep authentication and mutable application state outside the recipe. Provider model access must be checked on the destination account. @@ -66,16 +66,17 @@ GitHub Actions (`.github/workflows/ci.yml`) runs on pushes to `main`, on every p - `uv sync --locked --group dev`, then `ruff check` and `pytest`. - `./factory validate` for `config/default.yml` and `containers/factory.container.yml`. -- Audits of the Dockerfile, devcontainer, and Compose definitions (digest-pinned images, no host namespaces or socket, resource caps). +- Audits of the Dockerfile, devcontainer, and Compose definitions (the image builds from `ubuntu:latest`, Compose services are digest-pinned, no host namespaces or socket, resource caps). - A full worker image build and the behavior smoke in `tests/container-smoke.sh`. -Every action is pinned to an immutable commit SHA, and the token is read-only. The uv version comes from `toolchain.lock.json`, the same pin `./bootstrap.sh` installs. +Every action is pinned to an immutable commit SHA, and the token is read-only. uv and Bun are their latest releases, the same as `./bootstrap.sh` and `./factory apply` install. ## Primary sources - [Herdr installation](https://herdr.dev/docs/install/), [headless/SSH persistence](https://herdr.dev/docs/persistence-remote/), [session-state limits](https://herdr.dev/docs/session-state/), [config reference](https://herdr.dev/docs/config-reference/). -- [Herdr latest release](https://github.com/herdrdev/herdr/releases/latest). GitHub-hosted assets (herdr, bun, gh, no-mistakes, treehouse) are verified against the SHA-256 digest GitHub publishes for each release asset; no claim is made that a release supplies an independent SBOM or signature bundle. +- [Herdr latest release](https://github.com/herdrdev/herdr/releases/latest). GitHub-hosted assets (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura) are verified against the SHA-256 digest GitHub publishes for each release asset; no claim is made that a release supplies an independent SBOM or signature bundle. - [Node.js release index](https://nodejs.org/dist/index.json). Node assets are verified against the `SHASUMS256.txt` published beside each release. +- [rustup stable release](https://static.rust-lang.org/rustup/release-stable.toml). rustup-init is verified against the `.sha256` published beside it. - [Ansible introduction](https://docs.ansible.com/projects/ansible/latest/getting_started/index.html), [checksummed downloads](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/get_url_module.html), [user systemd/D-Bus requirements](https://docs.ansible.com/projects/ansible/latest/collections/ansible/builtin/systemd_service_module.html). - [systemd lingering](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html). - [Docker process boundaries](https://docs.docker.com/engine/containers/multi-service_container/), [Docker and host firewall behavior](https://docs.docker.com/engine/network/firewall-iptables/). diff --git a/docs/configuration.md b/docs/configuration.md index 8f40fb9..f7a9de7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,7 +7,7 @@ | Command | What it does | | --- | --- | | `./factory init` | Copies `config/default.yml` to `.local/host.yml` with your user, home, and `~/Dev` workspace filled in. Never overwrites an existing file. Options: `--user`, `--home`, `--container`. | -| `./factory validate` | Checks the config against `schemas/factory.schema.json` and the cross-field rules below, plus `toolchain.lock.json` against its schema. | +| `./factory validate` | Checks the config against `schemas/factory.schema.json` and the cross-field rules below, | | `./factory plan` | Runs the Ansible playbook in check mode. Reports what would change; mutates nothing. | | `./factory apply` | Runs the playbook for real. Asks for the sudo password when passwordless sudo is not available. With the `firstmate` profile on, the first successful interactive apply with omp signed in then opens the new-host questions (below). | | `./factory doctor` | Checks that each expected tool runs and reports `gh` authentication. Changes nothing. | @@ -42,9 +42,6 @@ factory: sidebar_agent_rows: | [ ... ] sidebar_bg: "reset" # the terminal's own background - browsers: # Obscura tier of the browser ladder - obscura_version: '0.2.2' - obscura_sha256: c1b4548e36549a0228c39c1cc842df425bc7253af2b0a56bd2a538d8ff7e3406 fleet: supabase_project_id: # The shared Supabase project id; the default is set in config/default.yml fixture_archive: "" # Path to DB volume tarball for fresh hosts @@ -59,7 +56,7 @@ factory: - `user` is not `root`, and `workspace` is inside `home`. - `firstmate` and `browser_prune.enabled` need `agents`. -- `fleet_guards` needs `docker` and `firstmate`, plus `browsers.obscura_version` and a 64-character hex `browsers.obscura_sha256`. +- `fleet_guards` needs `docker` and `firstmate`. Obscura is always its latest release; remove any `browsers.obscura_version` or `browsers.obscura_sha256` left in an older `.local/host.yml`, which validation now rejects. The Firstmate checkout tracks the default branch of upstream Firstmate, not a sha. Every apply fetches `origin/main` and fast-forwards `main`, so tracking it is how a host stays current. Do not re-pin it to a sha. Each run resolves `origin/main` once and reports the sha it installed. To track a fork, set `firstmate.url` in `.local/host.yml`. The URL applies to a fresh clone; verification fails when an existing checkout's `origin` is a different URL, and provisioning never changes it for you. To switch an existing checkout, run `git -C /firstmate remote set-url origin ` and `git -C /firstmate fetch origin`, reconcile any local commits on `main` with `origin/main` by hand, then run `./factory apply`. diff --git a/docs/dependencies.md b/docs/dependencies.md index 7471392..e5233cd 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -1,41 +1,26 @@ # Dependencies -Everything the recipe installs, grouped by the file that pins it. A version appears only where a file pins one; everything else tracks its upstream repository or latest release. `./factory plan` installs none of this. +Everything the recipe installs, grouped by where it comes from. Nothing is pinned: every `./factory apply` resolves each tool's newest release once and installs exactly that, so re-running apply upgrades an existing host. Every download is verified against the checksum its publisher posts for that exact release, and a release without one fails the apply instead of installing an unverified artifact. The installer records the releases it resolved in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. `./factory plan` installs none of this. ## Repository tooling -`bootstrap.sh`, `toolchain.lock.json`, `pyproject.toml`, `uv.lock` +`bootstrap.sh`, `pyproject.toml`, `uv.lock` -- uv 0.12.5 (sha256-locked), then `uv sync --locked`: ansible-core 2.21.4, jsonschema 4.26.0, PyYAML 6.0.3. +- The latest uv (below), then `uv sync --locked`: ansible-core 2.21.4, jsonschema 4.26.0, PyYAML 6.0.3. `uv.lock` is this repository's own development environment, so it stays locked. - Dev group: pytest 9.0.2, ruff 0.16.3. -## Pinned toolchain - -`toolchain.lock.json`, installed by `scripts/install_tools.py`. Every archive is sha256-locked and linked into `~/.local/bin`. - -- Always: uv 0.12.5. -- `development` profile: rustup-init 1.29.0, installing Rust 1.97.1 (minimal profile + rustfmt + clippy). - ## Latest releases -Not pinned: every `./factory apply` resolves the newest release once and installs exactly that, so re-running apply upgrades an existing host. The installer records the releases it installed in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. - -Native tools, linked into `~/.local/bin`. Each asset is verified against the SHA-256 its publisher lists for that exact release; a release that lists none fails the apply instead of installing an unverified binary. +`scripts/install_tools.py`. Native tools are linked into `~/.local/bin`; npm tools are each installed with `npm install` into `~/.local/share/code-factory//` (npm checks the registry integrity) and linked from there. Superseded versions stay on disk. -- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. +- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), uv ([astral-sh/uv](https://github.com/astral-sh/uv/releases/latest)), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. - Always: node, the newest release in the [nodejs.org index](https://nodejs.org/dist/index.json) (not the LTS line), verified against that release's `SHASUMS256.txt`. -- `agents` profile: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. - -The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); one apply makes five. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. - -npm tools, `agents` profile: the npm registry's `latest` version of omp (`@oh-my-pi/pi-coding-agent`), chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, and tasks-axi, each installed with `npm install` into `~/.local/share/code-factory//` (npm checks the registry integrity). Superseded versions stay on disk. The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. - -## Agent CLIs - -`tools/npm/package.json`, installed with `npm ci` from `tools/npm/package-lock.json` under the `agents` profile. +- `agents` profile, native: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. +- `agents` profile, npm: omp (`@oh-my-pi/pi-coding-agent`), chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, tasks-axi, acpx (runs the no-mistakes gate agent `acp:omp`), and chrome-devtools-mcp (the MCP build chrome-devtools-axi launches through `CHROME_DEVTOOLS_AXI_MCP_PATH`). The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. +- `agents` profile, omp plugins: ponytail, i-have-adhd and caveman from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. +- `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. -- acpx 0.18.0 (runs the no-mistakes gate agent `acp:omp`). -- chrome-devtools-mcp 1.9.0 (the MCP build chrome-devtools-axi launches through `CHROME_DEVTOOLS_AXI_MCP_PATH`). +The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); one resolution makes seven. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. ## Ubuntu packages @@ -52,14 +37,14 @@ npm tools, `agents` profile: the npm registry's `latest` version of omp (`@oh-my `fleet_guards` profile. -- Obscura `factory.browsers.obscura_version` (0.2.2 in `config/default.yml`) from github.com/h4ckf0r0day/obscura, verified against `factory.browsers.obscura_sha256` (`ansible/tasks/fleet-browsers.yml`). x86_64 only: `factory_browser_obscura_url` hardcodes the `obscura-x86_64-linux.tar.gz` asset, and the sha256 pins it. For aarch64 behavior, see [Browser ladder](fleet-guards.md#browser-ladder). -- Supabase CLI 2.117.0 (`fleet/shared-supabase/package.json`, `npm ci` from its lockfile). +- Obscura, the latest [h4ckf0r0day/obscura release](https://github.com/h4ckf0r0day/obscura/releases/latest) for the host's platform, verified against the GitHub release-asset digest (`ansible/tasks/fleet-browsers.yml`). Each release extracts into its own `~/oss-fleet/browsers/obscura-/`. +- Supabase CLI, the npm registry's latest `supabase`, installed with `npm install` into `~/oss-fleet/shared-supabase` (`ansible/tasks/fleet_guards.yml`). ## Chrome autopruner `agents` profile with `browser_prune.enabled`. -- psutil 7.1.0, hash-pinned into a private venv (`maintenance/requirements.txt`, `ansible/tasks/browser_prune.yml`). +- psutil, the latest PyPI release, installed into a private venv with `uv pip install --require-hashes` against the SHA-256 digests PyPI publishes for that release (`ansible/tasks/browser_prune.yml`). ## Firstmate @@ -71,8 +56,8 @@ npm tools, `agents` profile: the npm registry's `latest` version of omp (`@oh-my `Dockerfile`, `compose.yml` -- Worker base `ubuntu:24.04@sha256:224a1869…` plus apt: bash, build-essential, ca-certificates, curl, git, iproute2, jq, less, libssl-dev, openssh-client, pkg-config, procps, python3, python3-apt, python3-venv, sudo, tar, unzip, xz-utils, zstd. -- Optional compose backing services (digest-pinned): `postgres:18-bookworm`, `redis:8-alpine`. +- Worker base `ubuntu:latest` (the newest Ubuntu LTS) plus apt: bash, build-essential, ca-certificates, curl, git, iproute2, jq, less, libssl-dev, openssh-client, pkg-config, procps, python3, python3-apt, python3-venv, sudo, tar, unzip, xz-utils, zstd. +- Optional compose backing services, never installed by apply: `postgres:18-bookworm`, `redis:8-alpine`, digest-pinned because a floating Postgres tag would move a data volume across major versions it cannot read. ## Assumed on the host diff --git a/docs/fleet-guards.md b/docs/fleet-guards.md index c208c65..e983beb 100644 --- a/docs/fleet-guards.md +++ b/docs/fleet-guards.md @@ -23,7 +23,7 @@ Sizing per lane count and the full list of pruners are in | Path (under the account home) | Purpose | | --- | --- | -| `oss-fleet/shared-supabase/` | The ONE stack: pinned CLI (`npm ci` from `fleet/shared-supabase/package-lock.json`), `supabase/config.toml` with `factory.fleet.supabase_project_id`, `check.sh` keeper, `guard.sql`, `README.md`. `check.sh` also generates the project's `.env.local` from the running stack. | +| `oss-fleet/shared-supabase/` | The ONE stack: the latest Supabase CLI (`npm install` of the registry's latest `supabase`), `supabase/config.toml` with `factory.fleet.supabase_project_id`, `check.sh` keeper, `guard.sql`, `README.md`. `check.sh` also generates the project's `.env.local` from the running stack. | | `oss-fleet/doctor/docker-guard.sh` | `docker events` watcher. A container carrying `com.supabase.cli.project` other than an allowlisted project is removed on creation; bare Postgres-family images are logged and alerted, not killed (other projects may own them). `docker-guard-allow.txt` is written once and then operator-owned. | | `oss-fleet/doctor/worktree-env-seed.sh` | Installs the env file as `.env.local` in the project worktrees matched by the pool glob and Firstmate checkout path fixed in `fleet/doctor/worktree-env-seed.sh`; `factory.fleet.worktree_pools` only sets which pool directories the systemd path unit watches to trigger it. Files without the `# fleet-shared-supabase` marker are replaced with a backup left beside them. | | `oss-fleet/doctor/dev-server-reaper.sh` | Every 2 minutes: kills `next dev`/`next-server`/`tsc --noEmit` trees in treehouse worktrees whose lane last reported `done:`/`paused:`/`blocked:`/`failed:`, has no agent process, or whose agent transcript is idle >= 30 min (`REAPER_IDLE_MIN`). A dev server is 3-4 GB and restarts in 10 s; idle ones from finished lanes are what filled swap. One `next dev` per branch is inherent - Next compiles the whole app per process - so the fix is lifetime, not sharing. | @@ -120,11 +120,8 @@ connected). Tier 3 needs the `desktop` profile's TigerVNC/noVNC packages Chrome/Chromium binary (`FLEET_CHROME_BIN`, Google Chrome, Chromium, or a Playwright Chromium). -Obscura ships for x86_64 only (see [Dependencies](dependencies.md#fleet-browsers-and-supabase)). -The defaults do not change on aarch64: `fleet/browsers/env.sh` and -`herdr.service` still point at obscura on `:9222`, so tier 1 never answers -there. An agent has to escalate by hand with `fleet-browser up chrome` and -`eval "$(fleet-browser env chrome)"`. +Obscura ships for x86_64 and aarch64; apply installs the latest release for the +host's platform (see [Dependencies](dependencies.md#fleet-browsers-and-supabase)). All three tiers share one session. `cookie-sync.ts` keeps a canonical jar at `~/.fleet-browser/cookies.json` and converges every live tier to it over CDP. diff --git a/docs/omp.md b/docs/omp.md index e460132..a6da662 100644 --- a/docs/omp.md +++ b/docs/omp.md @@ -6,7 +6,7 @@ omp (`@oh-my-pi/pi-coding-agent`) is the agent harness Code Factory installs. Th With the `agents` profile on, `./factory apply`: -1. Installs the latest published omp, resolved from the npm registry on every apply (see [Dependencies](dependencies.md#latest-releases)). +1. Installs the latest published omp, resolved from the npm registry on every apply (see [Dependencies](dependencies.md#latest-releases)), plus the omp plugins ponytail, i-have-adhd and caveman, upgraded to their latest release on every apply. 2. Copies [`config/omp.yml`](../config/omp.yml) to `~/.omp/agent/config.yml` (directory `0700`, file `0600`), and [`config/omp-lsp.json`](../config/omp-lsp.json) to `~/.omp/agent/lsp.json`, which disables the markdown language server (marksman): it costs each session about 90 MB, and markdown diagnostics add nothing to agent work. 3. Installs the extension `~/.omp/agent/extensions/code-factory-herdr-sidebar.ts`, which feeds the Herdr Agent sidebar the session topic, the pane's short name, and the pull request line (pull request, issue and diff size). Every apply rewrites it. See [Herdr sidebar](herdr.md). 4. Installs the extension `~/.omp/agent/extensions/aa-mode-icons.ts` from [`config/omp-status-icons.ts`](../config/omp-status-icons.ts). Every apply rewrites it. See [Status line icons](#status-line-icons). diff --git a/docs/recovery.md b/docs/recovery.md index 22706f5..ab47b48 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -56,7 +56,7 @@ Never upload a raw failed HTTP request to a public issue or this repository. Suc ## Drift and upgrades -Run `./factory plan` before applying changed pins. Only uv and the Rust toolchain are pinned: every apply upgrades every other tool to its latest release, and a herdr upgrade restarts `herdr.service`. An unmanaged executable at a managed command path is a refusal, not permission to overwrite it. Firstmate refuses dirty or independently advanced checkouts. Preserve that work and decide whether to update the configuration's pin or move to a separate clean checkout. +Run `./factory plan` before applying changed pins. Nothing is pinned: every apply upgrades every tool to its latest release, and a herdr upgrade restarts `herdr.service`. An unmanaged executable at a managed command path is a refusal, not permission to overwrite it. Firstmate refuses dirty or independently advanced checkouts. Preserve that work and decide whether to update the configuration's pin or move to a separate clean checkout. Existing OMP settings are first-write-only: provisioning will not replace provider configuration or credentials on a reused account. Review and merge the exported safe preferences manually if deliberately updating an established account. diff --git a/docs/security.md b/docs/security.md index 57fcf63..c162245 100644 --- a/docs/security.md +++ b/docs/security.md @@ -37,6 +37,6 @@ This export does not rewrite the current host's firewall, SSH policy, account me ## Updates -Tool versions and artifact checksums belong in reviewed lock files. Package-manager integrity checks verify the downloaded package matches the lock; they do not establish that a publisher is trustworthy. Review added dependencies and installer behavior before updating locks. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. +Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (GitHub release-asset digests, Node's `SHASUMS256.txt`, rustup's `.sha256`, npm registry integrity, PyPI digests); a release without one is refused. That proves the download is the published artifact; it does not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. Back up project repositories and application data separately, using encrypted storage and an application-aware restore procedure. A successful environment bootstrap is not evidence that a database backup is recoverable. diff --git a/fleet/shared-supabase/package-lock.json b/fleet/shared-supabase/package-lock.json deleted file mode 100644 index ccf9ee0..0000000 --- a/fleet/shared-supabase/package-lock.json +++ /dev/null @@ -1,231 +0,0 @@ -{ - "name": "fleet-shared-supabase", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "fleet-shared-supabase", - "dependencies": { - "supabase": "2.117.0" - } - }, - "node_modules/@ecies/ciphers": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", - "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", - "license": "MIT", - "engines": { - "bun": ">=1", - "deno": ">=2.7.10", - "node": ">=16" - }, - "peerDependencies": { - "@noble/ciphers": "^1.0.0" - } - }, - "node_modules/@noble/ciphers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", - "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@supabase/cli-darwin-arm64": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.117.0.tgz", - "integrity": "sha512-MQVIxL+B5/U9xiXcUtky0vJQ5Qe0qhGkK2JLGwi5BAo5XxcC+iP+KCsVB1/2zzYT+d+fOlZnKyTFFWYO6Ut1sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@supabase/cli-darwin-x64": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.117.0.tgz", - "integrity": "sha512-NMuY3n8Bfe+sPCdrUQQSFkOLFqlQYkg1C0SuqvJG4HVU8ukZ7IH2gy+HFGXNuVitFPapmHTPT8NjbZ+YrunZQw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@supabase/cli-linux-arm64": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.117.0.tgz", - "integrity": "sha512-CcMHEB8znY9usMJ2dwAKQV06XIYHzLgnq1KY6HzVT9XmXo0qZxTifC/oWgHX39SmCFm7a3HR4K5J95ysYz+8Hg==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@supabase/cli-linux-arm64-musl": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.117.0.tgz", - "integrity": "sha512-n5yZuBbBD5MnWqczezH72GAEfdTBHBk9Ei/pgpCnAJtClei9QQsiN1UZNw2A7K1EP4VWxgO/YZr3gFYgFFH1WQ==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@supabase/cli-linux-x64": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.117.0.tgz", - "integrity": "sha512-qDJEURD/YajKD/OQ91O9XNHB/LHK9V8xZ9qGiUQCFhP9OAvMQjUT12j8k/mgTe4PqIxigGpfvm+eNxRcKnU9Rw==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@supabase/cli-linux-x64-musl": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.117.0.tgz", - "integrity": "sha512-rlVDN5kXTOe+50wFN7DywFFx956XoYpEJpDkADvZ3k+qL5+7jGxSzQN/7xhHqlhjtSlHfx4QlxajfOerJNnrlg==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@supabase/cli-windows-arm64": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.117.0.tgz", - "integrity": "sha512-JNjI4dHhogjx1994jNoV9uUMioCrdzfdDJDeq0rZhYEepBGE9W+2gLniBQfQRzGxXK81mg5K3p9CTJYBZzbAKg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@supabase/cli-windows-x64": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.117.0.tgz", - "integrity": "sha512-oFmeLMwgWAyQhzPoR6w+eUOfyJcRMNHN2Ju4BxBtpZ9vW+dO1bAsWd7aVVcpn1sgHA/ptM8U48P+se5yZR7BdQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/eciesjs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.5.0.tgz", - "integrity": "sha512-s0J9SEVYAEPg7J63GFMApLYzPH9VNIQIyC6s15JpnqVc0TqcKWdbgFlnAweEBRyMmko2dcs2sfC83Hj4J43tuA==", - "license": "MIT", - "dependencies": { - "@ecies/ciphers": "^0.2.6", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "^1.9.7", - "@noble/hashes": "^1.8.0" - }, - "engines": { - "bun": ">=1", - "deno": ">=2.7.10", - "node": ">=16" - } - }, - "node_modules/jose": { - "version": "6.2.12", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", - "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/supabase": { - "version": "2.117.0", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.117.0.tgz", - "integrity": "sha512-V4UDf/kgrqllHbc22vs89bL7pdBj3xhVolvDflByS5FuJb+tXpWhHZxYEbBQspxA0fBzaj+lpiedB72xTAZA4w==", - "license": "MIT", - "dependencies": { - "eciesjs": "^0.5.0", - "jose": "^6.2.10" - }, - "bin": { - "supabase": "dist/supabase.js" - }, - "optionalDependencies": { - "@supabase/cli-darwin-arm64": "2.117.0", - "@supabase/cli-darwin-x64": "2.117.0", - "@supabase/cli-linux-arm64": "2.117.0", - "@supabase/cli-linux-arm64-musl": "2.117.0", - "@supabase/cli-linux-x64": "2.117.0", - "@supabase/cli-linux-x64-musl": "2.117.0", - "@supabase/cli-windows-arm64": "2.117.0", - "@supabase/cli-windows-x64": "2.117.0" - } - } - } -} diff --git a/fleet/shared-supabase/package.json b/fleet/shared-supabase/package.json deleted file mode 100644 index 7b4b0df..0000000 --- a/fleet/shared-supabase/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "fleet-shared-supabase", - "private": true, - "description": "Pinned Supabase CLI for the ONE shared local stack (Code Factory fleet guards)", - "dependencies": { - "supabase": "2.117.0" - } -} diff --git a/maintenance/requirements.txt b/maintenance/requirements.txt deleted file mode 100644 index df9e82a..0000000 --- a/maintenance/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -psutil==7.1.0 \ - --hash=sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca \ - --hash=sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3 \ - --hash=sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d \ - --hash=sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5 \ - --hash=sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2 \ - --hash=sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07 \ - --hash=sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13 \ - --hash=sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d \ - --hash=sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3 diff --git a/schemas/factory.schema.json b/schemas/factory.schema.json index 996e69a..b448c23 100644 --- a/schemas/factory.schema.json +++ b/schemas/factory.schema.json @@ -257,22 +257,11 @@ "type": "object", "additionalProperties": false, "properties": { - "obscura_version": { - "type": "string" - }, - "obscura_sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, "idle_timeout_min": { "type": "integer", "minimum": 5 } - }, - "required": [ - "obscura_version", - "obscura_sha256" - ] + } } }, "required": [ diff --git a/schemas/toolchain.schema.json b/schemas/toolchain.schema.json deleted file mode 100644 index 35d948e..0000000 --- a/schemas/toolchain.schema.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "additionalProperties": false, - "properties": { - "schema_version": { - "const": 1 - }, - "tools": { - "type": "object", - "required": [ - "uv", - "rustup-init" - ], - "propertyNames": { - "enum": [ - "uv", - "rustup-init" - ] - }, - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "version": { - "type": "string", - "pattern": "^[A-Za-z0-9_.-]+$" - }, - "assets": { - "type": "object", - "additionalProperties": false, - "properties": { - "linux-x86_64": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "string", - "pattern": "^https://[^ @]+$" - }, - "sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "format": { - "enum": [ - "file", - "tar", - "zip" - ] - }, - "binaries": { - "type": "object", - "minProperties": 1, - "propertyNames": { - "pattern": "^[A-Za-z0-9_.-]+$" - }, - "additionalProperties": { - "type": "string", - "minLength": 1 - } - } - }, - "required": [ - "url", - "sha256", - "format", - "binaries" - ] - }, - "linux-aarch64": { - "type": "object", - "additionalProperties": false, - "properties": { - "url": { - "type": "string", - "pattern": "^https://[^ @]+$" - }, - "sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "format": { - "enum": [ - "file", - "tar", - "zip" - ] - }, - "binaries": { - "type": "object", - "minProperties": 1, - "propertyNames": { - "pattern": "^[A-Za-z0-9_.-]+$" - }, - "additionalProperties": { - "type": "string", - "minLength": 1 - } - } - }, - "required": [ - "url", - "sha256", - "format", - "binaries" - ] - } - }, - "required": [ - "linux-x86_64", - "linux-aarch64" - ] - } - }, - "required": [ - "version", - "assets" - ] - } - }, - "rust_toolchain": { - "type": "string", - "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" - } - }, - "required": [ - "schema_version", - "tools", - "rust_toolchain" - ] -} diff --git a/scripts/factory.py b/scripts/factory.py index 42d9c64..01b5f08 100755 --- a/scripts/factory.py +++ b/scripts/factory.py @@ -48,20 +48,6 @@ def validate_config(document): if config["profiles"].get("fleet_guards"): if not (config["profiles"]["docker"] and config["profiles"]["firstmate"]): raise ValueError("fleet guards require the docker and firstmate profiles") - browsers = config.get("browsers") - if ( - not browsers - or not browsers.get("obscura_version") - or not browsers.get("obscura_sha256") - ): - raise ValueError( - "fleet_guards requires factory.browsers.obscura_version and obscura_sha256" - ) - sha = browsers["obscura_sha256"] - if len(sha) != 64 or not all(c in "0123456789abcdef" for c in sha.lower()): - raise ValueError( - "factory.browsers.obscura_sha256 must be a 64-char lowercase hex string" - ) fixture = config.get("fleet", {}).get("fixture_archive", "") if fixture and ".." in Path(fixture).parts: raise ValueError("fleet.fixture_archive must not traverse; give a plain path") @@ -73,8 +59,6 @@ def validate_config(document): or prune["idle_seconds"] < 2 * prune["poll_seconds"] ): raise ValueError("pruning idle/gap windows must allow at least two observation intervals") - lock = json.loads((ROOT / "toolchain.lock.json").read_text()) - validate_document(lock, "toolchain.schema.json") return document @@ -321,7 +305,7 @@ def main(): ) document = load_config(path.resolve()) if args.command == "validate": - print(f"Valid host configuration and artifact lock: {path}") + print(f"Valid host configuration: {path}") return 0 if args.command == "doctor": return doctor(document) diff --git a/scripts/install_tools.py b/scripts/install_tools.py index df157ac..57789c7 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -4,11 +4,11 @@ Stdlib-only: also bootstraps uv before repository dependencies exist. stdout is one JSON result; installer progress goes to stderr. Existing unmanaged commands are never replaced. Archives cannot write outside their staging directory. -uv, rustup-init and the Rust toolchain are pinned in the lock. Every other tool -tracks its latest release: native assets are verified against the SHA-256 their -publisher lists for that release (the GitHub release-asset digest, or Node's -SHASUMS256.txt), npm tools against the integrity npm records for the resolved -version. +Nothing is pinned: every tool tracks its latest release. Native assets are +verified against the SHA-256 their publisher lists for that exact release (the +GitHub release-asset digest, Node's SHASUMS256.txt, rustup's .sha256), npm tools +against the integrity npm records for the resolved version, psutil against the +digests PyPI publishes. The Rust toolchain follows the stable channel. """ import argparse @@ -33,8 +33,8 @@ GITHUB_API = "https://api.github.com/repos/{}/releases/latest" # How release asset names spell each platform. ARCH = { - "linux-x86_64": {"node": "x64", "go": "amd64", "bun": "x64-baseline"}, - "linux-aarch64": {"node": "arm64", "go": "arm64", "bun": "aarch64"}, + "linux-x86_64": {"node": "x64", "go": "amd64", "bun": "x64-baseline", "gnu": "x86_64"}, + "linux-aarch64": {"node": "arm64", "go": "arm64", "bun": "aarch64", "gnu": "aarch64"}, } # Native tools on their latest GitHub release: repository, tag prefix, asset # name, and where each command sits inside the asset. @@ -54,6 +54,19 @@ "treehouse-v{v}-linux-{go}.tar.gz", {"treehouse": "treehouse"}, ), + "uv": ( + "astral-sh/uv", + "", + "uv-{gnu}-unknown-linux-gnu.tar.gz", + {"uv": "uv-*/uv", "uvx": "uv-*/uvx"}, + ), + # Resolved for the fleet browser ladder (ansible/tasks/fleet-browsers.yml), not installed here. + "obscura": ( + "h4ckf0r0day/obscura", + "v", + "obscura-{gnu}-linux.tar.gz", + {"obscura": "obscura", "obscura-worker": "obscura-worker"}, + ), } # npm tools on the registry's latest version, each installed into its own prefix. NPM_LATEST = { @@ -63,6 +76,8 @@ "lavish-axi": "lavish-axi", "quota-axi": "quota-axi", "tasks-axi": "tasks-axi", + "acpx": "acpx", + "chrome-devtools-mcp": "chrome-devtools-mcp", } # Native tools the agents profile adds. AGENT_TOOLS = ["gh", "no-mistakes", "treehouse"] @@ -139,9 +154,29 @@ def resolve_latest(key): match[1] if match else "", binaries, ) + toml = fetch("https://static.rust-lang.org/rustup/release-stable.toml", "rustup").decode() + match = re.search(r"^version = '([0-9.]+)'$", toml, re.M) + if not match: + raise ValueError("cannot read the latest rustup version") + version = match[1] + url = f"https://static.rust-lang.org/rustup/archive/{version}/{ARCH[key]['gnu']}-unknown-linux-gnu/rustup-init" + checksum = fetch(url + ".sha256", "rustup-init checksum").decode().split(" ")[0] + binaries = {"rustup-init": "rustup-init"} + latest["rustup-init"] = verified( + "rustup-init", version, key, "rustup-init", url, checksum, binaries + ) for tool, package in NPM_LATEST.items(): registry = fetch(f"https://registry.npmjs.org/{package}/latest", f"{tool} version") latest[tool] = json.loads(registry)["version"] + # Resolved for the fleet guards' shared Supabase stack, installed by Ansible. + registry = fetch("https://registry.npmjs.org/supabase/latest", "supabase version") + latest["supabase"] = json.loads(registry)["version"] + # The Chrome pruner's runtime, installed by Ansible with uv --require-hashes. + pypi = json.loads(fetch("https://pypi.org/pypi/psutil/json", "psutil release")) + hashes = [file["digests"]["sha256"] for file in pypi["urls"]] + if not hashes or not all(re.fullmatch(r"[0-9a-f]{64}", h) for h in hashes): + raise ValueError("psutil publishes no SHA-256 digests; refusing an unverified binary") + latest["psutil"] = {"version": pypi["info"]["version"], "sha256": hashes} return latest @@ -266,39 +301,6 @@ def command(argv, environment): subprocess.run([str(arg) for arg in argv], env=environment, check=True, stdout=sys.stderr) -def npm_install(repo, home, environment): - source = repo / "tools/npm" - manifest = json.loads((source / "package.json").read_text()) - lock_hash = digest(source / "package-lock.json") - destination = home / ".local/share/code-factory/npm" - stamp = destination / ".code-factory-lock" - expected = lock_hash + ":" + digest(source / "package.json") - installed = stamp.is_file() and stamp.read_text().strip() == expected - for name, version in manifest["dependencies"].items(): - package = destination / "node_modules" / name / "package.json" - if not package.is_file() or json.loads(package.read_text()).get("version") != version: - installed = False - changed = False - if not installed: - if ( - destination.exists() - and any(destination.iterdir()) - and not stamp.exists() - and not (destination / "package-lock.json").exists() - ): - raise ValueError(f"refusing unmanaged npm prefix: {destination}") - destination.mkdir(parents=True, exist_ok=True) - for name in ("package.json", "package-lock.json"): - shutil.copyfile(source / name, destination / name) - command( - [home / ".local/bin/npm", "ci", "--prefix", destination, "--no-audit", "--no-fund"], - environment, - ) - stamp.write_text(expected + "\n") - changed = True - return link_package_bins(home, destination, manifest["dependencies"]) or changed - - def link_package_bins(home, destination, package_names): # Only expose explicitly requested packages, not incidental dependency bins. changed = False @@ -361,56 +363,32 @@ def prune_dangling_links(home): return changed -def rust_install(home, version, environment): +def rust_install(home, environment): + """The stable toolchain with rustfmt and clippy; an apply moves it to the newest stable.""" + version = "stable" rustup = home / ".cargo/bin/rustup" environment = { **environment, "CARGO_HOME": str(home / ".cargo"), "RUSTUP_HOME": str(home / ".rustup"), } + profile = ["--profile", "minimal", "--component", "rustfmt", "--component", "clippy"] changed = False if not rustup.exists(): - command( - [ - home / ".local/bin/rustup-init", - "-y", - "--no-modify-path", - "--profile", - "minimal", - "--default-toolchain", - version, - "--component", - "rustfmt", - "--component", - "clippy", - ], - environment, - ) + init = [home / ".local/bin/rustup-init", "-y", "--no-modify-path", "--default-toolchain"] + command([*init, version, *profile], environment) changed = True else: + # ponytail: the rustup binary itself only changes on a fresh account; + # add `rustup self update` here if an old rustup ever matters. result = subprocess.run( - [rustup, "run", version, "rustc", "--version"], + [rustup, "toolchain", "install", version, *profile, "--no-self-update"], env=environment, capture_output=True, text=True, + check=True, ) - if result.returncode or not result.stdout.startswith(f"rustc {version} "): - command( - [ - rustup, - "toolchain", - "install", - version, - "--profile", - "minimal", - "--component", - "rustfmt", - "--component", - "clippy", - ], - environment, - ) - changed = True + changed = " unchanged - " not in result.stdout components = subprocess.run( [rustup, "component", "list", "--installed", "--toolchain", version], env=environment, @@ -445,9 +423,44 @@ def rust_install(home, version, environment): return changed +# omp marketplace plugins, kept at their latest release: name -> GitHub repository. +OMP_PLUGINS = { + "ponytail": "DietrichGebert/ponytail", + "i-have-adhd": "ayghri/i-have-adhd", + "caveman": "JuliusBrussee/caveman", +} + + +def omp_plugins(home, environment): + """Install each plugin from its marketplace, then upgrade all to the latest release.""" + + def omp(*argv): + result = subprocess.run( + [home / ".local/bin/omp", "plugin", *argv], + env=environment, + capture_output=True, + text=True, + check=True, + ) + return result.stdout + + changed = False + marketplaces = home / ".omp/marketplaces.json" + known = marketplaces.read_text() if marketplaces.is_file() else "" + installed = {plugin["id"] for plugin in json.loads(omp("list", "--json"))["marketplace"]} + for name, repository in OMP_PLUGINS.items(): + if repository not in known: + omp("marketplace", "add", repository) + changed = True + if f"{name}@{name}" not in installed: + omp("install", f"{name}@{name}") + changed = True + omp("marketplace", "update") + return "up to date" not in omp("upgrade") or changed + + def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--lock", type=Path, required=True) parser.add_argument("--home", type=Path, required=True) parser.add_argument("--tools", default="herdr,node,bun,uv") parser.add_argument("--npm", action="store_true") @@ -466,14 +479,10 @@ def main(): home = args.home.resolve(strict=True) if home.stat().st_uid != os.geteuid(): parser.error("run as the user who owns --home") - lock = json.loads(args.lock.read_text()) - if lock.get("schema_version") != 1: - parser.error("unsupported toolchain lock schema") names = list(dict.fromkeys(args.tools.split(",") + (AGENT_TOOLS if args.npm else []))) if args.development: names.append("rustup-init") - wanted = args.npm or any(name == "node" or name in GITHUB_LATEST for name in names) - latest = args.resolved or (resolve_latest(key) if wanted else {}) + latest = args.resolved or resolve_latest(key) environment = { **os.environ, "HOME": str(home), @@ -485,19 +494,17 @@ def main(): fcntl.flock(guard, fcntl.LOCK_EX) changed = False for name in names: - spec = latest[name] if name in latest else lock["tools"][name] - changed = install_asset(home, name, spec, key) or changed + changed = install_asset(home, name, latest[name], key) or changed if args.npm: - changed = npm_install(args.lock.resolve().parent, home, environment) or changed for tool in NPM_LATEST: changed = npm_latest_install(home, tool, latest[tool], environment) or changed changed = prune_dangling_links(home) or changed + changed = omp_plugins(home, environment) or changed if args.development: - changed = rust_install(home, lock["rust_toolchain"], environment) or changed + changed = rust_install(home, environment) or changed # The record checks compare against, so a later upstream release cannot # make an unchanged install look wrong. - if latest: - (prefix / "resolved.json").write_text(json.dumps(latest, sort_keys=True) + "\n") + (prefix / "resolved.json").write_text(json.dumps(latest, sort_keys=True) + "\n") print( json.dumps( { diff --git a/tests/container-smoke.sh b/tests/container-smoke.sh index e741078..dd0b758 100755 --- a/tests/container-smoke.sh +++ b/tests/container-smoke.sh @@ -10,8 +10,8 @@ # Container mode (inside the image; CMD of the `smoke` target): # tests/container-smoke.sh --in-container [--only NAME,NAME] # Exercises what the image actually contains, through the programs a user -# would run: managed tool versions compared with toolchain.lock.json, herdr -# and omp compared with the release the build resolved, a Herdr +# would run: managed tool versions compared with the releases the build +# resolved, a Herdr # configuration that Herdr itself accepts and that matches the factory # document, the user unit's ExecStart resolved and executed, a real headless # `herdr server` brought up and shut down over its API socket, the repository @@ -208,12 +208,7 @@ cf_python() { fi } -lock_version() { - jq -er --arg tool "$1" '.tools[$tool].version' "${CF_ROOT}/toolchain.lock.json" -} - -# Every tool except uv and the Rust toolchain tracks its latest release. The -# installer records the releases it installed in RESOLVED_STAMP; checks compare +# Every tool tracks its latest release. The installer records the releases it installed in RESOLVED_STAMP; checks compare # against that record, so an upstream release published after the build cannot # turn them red. RESOLVED_STAMP="${HOME}/.local/share/code-factory/resolved.json" @@ -232,7 +227,7 @@ herdr_installed_bin() { # has since overtaken; that is an upgrade, not a repeat change. image_is_current() { local latest versions='map_values(.version? // .)' - latest=$(python3 "${CF_ROOT}/scripts/install_tools.py" --lock "${CF_ROOT}/toolchain.lock.json" \ + latest=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ --home "${HOME}" --resolve) || fail "could not resolve the latest releases" [ "$(jq -cS "${versions}" <<<"${latest}")" = "$(jq -cS "${versions}" "${RESOLVED_STAMP}")" ] \ && return 0 @@ -328,13 +323,8 @@ check_core_tools() { version=$("${bin}" --version 2>&1) || fail "${tool} --version exited nonzero: ${version}" version=${version%%$'\n'*} [ -n "${version}" ] || fail "${tool} --version produced no output" - # An existing command is not enough: it must be the build the lock pins - # (uv) or the release the build resolved (node, bun). - if [ "${tool}" = uv ]; then - expected=$(lock_version uv) || fail "toolchain.lock.json pins no version for uv" - else - expected=$(resolved_version ".${tool}.version") || fail "${RESOLVED_STAMP} records no ${tool}" - fi + # An existing command is not enough: it must be the release the build resolved. + expected=$(resolved_version ".${tool}.version") || fail "${RESOLVED_STAMP} records no ${tool}" case "${version}" in *"${expected}"*) ;; *) fail "${tool} reports '${version}' but expected ${expected}" ;; @@ -343,7 +333,8 @@ check_core_tools() { done } -# The installed herdr, gh, no-mistakes, treehouse, omp and AXI tools are exactly +# The installed herdr, gh, no-mistakes, treehouse, omp, AXI tools, acpx and +# chrome-devtools-mcp are exactly # the releases the build resolved and recorded in RESOLVED_STAMP. check_resolved_releases() { local tool version banner package prefix @@ -357,7 +348,8 @@ check_resolved_releases() { esac printf '%s %s\n' "${tool}" "${version}" done - for tool in omp:@oh-my-pi/pi-coding-agent chrome-devtools-axi gh-axi lavish-axi quota-axi tasks-axi; do + for tool in omp:@oh-my-pi/pi-coding-agent chrome-devtools-axi gh-axi lavish-axi quota-axi tasks-axi acpx \ + chrome-devtools-mcp; do package=${tool#*:}; tool=${tool%%:*} version=$(resolved_version ".\"${tool}\"") prefix="${HOME}/.local/share/code-factory/${tool}/${version}" @@ -391,26 +383,6 @@ check_herdr_install_layout() { printf 'herdr symlink -> %s\n' "${target}" } -check_npm_tooling() { - local npm_root link target linked=0 - npm_root="${HOME}/.local/share/code-factory/npm" - [ -d "${npm_root}" ] || fail "agents profile selected but ${npm_root} is missing" - for link in "${HOME}"/.local/bin/*; do - [ -L "${link}" ] || continue - target=$(readlink -f "${link}" 2>/dev/null || true) - case "${target}" in - "${npm_root}"/*) - [ -x "${target}" ] || fail "npm-linked command ${link} resolves to non-executable ${target}" - linked=$((linked + 1)) - ;; - esac - done - [ "${linked}" -ge 1 ] || fail "no .local/bin command links point into ${npm_root}" - # Support binaries are only probed for presence: no agent CLI is executed - # here, so nothing can trigger first-run authentication or profile creation. - printf '%s npm-linked commands resolve into %s\n' "${linked}" "${npm_root}" -} - check_development_toolchain() { local tool path version for tool in rustc cargo; do @@ -681,7 +653,6 @@ check_factory_init_refuses_overwrite() { check_installer_idempotent() { local out out=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ - --lock "${CF_ROOT}/toolchain.lock.json" \ --home "${HOME}" \ --tools herdr,node,bun,uv \ --resolved "$(cat "${RESOLVED_STAMP}")" \ @@ -790,7 +761,10 @@ check_agent_gate() { printf '%s\n%s\n' "${floor}" "${version}" | sort -C -V || fail "${name} ${version} is below the fleet floor ${floor}" printf '%s %s (floor %s)\n' "${name}" "${version}" "${floor}" done - printf 'gate agent acp:omp via acpx, ponytail-review on omp\n' + for name in ponytail i-have-adhd caveman; do + [ -d "${HOME}/.omp/plugins/cache/marketplaces/${name}" ] || fail "omp plugin ${name} is not installed" + done + printf 'gate agent acp:omp via acpx, ponytail-review on omp, omp plugins installed\n' } container_mode() { @@ -810,7 +784,6 @@ container_mode() { run_check resolved-releases check_resolved_releases run_check herdr-install-layout check_herdr_install_layout run_check herdr-unit check_herdr_unit - run_check npm-tooling check_npm_tooling run_check development-toolchain check_development_toolchain run_check login-shell-environment check_login_shell_environment run_check agent-gate check_agent_gate diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 19be82c..8d3b9be 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -67,13 +67,6 @@ def test_fleet_guards_accept_the_default_document_when_enabled(configuration): assert factory.validate_config(configuration) is configuration -def test_fleet_guards_require_browsers_block(configuration): - configuration["factory"]["profiles"]["fleet_guards"] = True - configuration["factory"].pop("browsers", None) - with pytest.raises(ValueError, match="obscura"): - factory.validate_config(configuration) - - def test_browsers_valid_block_accepted(configuration): assert factory.validate_config(configuration) is configuration @@ -100,7 +93,6 @@ def test_bad_polling_window_cannot_disable_idle_accrual(configuration): def test_init_preserves_existing_local_configuration(tmp_path, monkeypatch): for path in ("config", "schemas"): shutil.copytree(ROOT / path, tmp_path / path) - shutil.copyfile(ROOT / "toolchain.lock.json", tmp_path / "toolchain.lock.json") monkeypatch.setattr(factory, "ROOT", tmp_path) args = argparse.Namespace(user="coder", home="/home/coder", container=True) factory.initialize(args) @@ -117,7 +109,6 @@ def test_init_preserves_existing_local_configuration(tmp_path, monkeypatch): def test_root_operator_is_rejected_before_config_is_written(tmp_path, monkeypatch): for path in ("config", "schemas"): shutil.copytree(ROOT / path, tmp_path / path) - shutil.copyfile(ROOT / "toolchain.lock.json", tmp_path / "toolchain.lock.json") monkeypatch.setattr(factory, "ROOT", tmp_path) with pytest.raises(ValueError, match="non-root"): factory.initialize(argparse.Namespace(user="root", home="/home/root", container=False)) diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 218c824..929efa9 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -103,11 +103,13 @@ def test_download_rejects_plain_http(tmp_path): "cli/cli": ("v9.9.9", "gh_9.9.9_linux_amd64.tar.gz"), "kunchenguid/no-mistakes": ("v9.9.9", "no-mistakes-v9.9.9-linux-amd64.tar.gz"), "kunchenguid/treehouse": ("v9.9.9", "treehouse-v9.9.9-linux-amd64.tar.gz"), + "astral-sh/uv": ("9.9.9", "uv-x86_64-unknown-linux-gnu.tar.gz"), + "h4ckf0r0day/obscura": ("v9.9.9", "obscura-x86_64-linux.tar.gz"), } def upstream(monkeypatch, unverified=None, seen=None): - """Fake GitHub, nodejs.org and npm; `unverified` publishes no SHA-256 for that source.""" + """Fake GitHub, nodejs.org, rustup, npm and PyPI; `unverified` publishes no SHA-256 for that source.""" def urlopen(request, **kwargs): url = request.full_url @@ -130,6 +132,14 @@ def urlopen(request, **kwargs): elif url == "https://nodejs.org/dist/v30.1.0/SHASUMS256.txt": sums = "" if unverified == "node" else "b" * 64 + " node-v30.1.0-linux-x64.tar.xz\n" return io.BytesIO(("c" * 64 + " node-v30.1.0-linux-arm64.tar.xz\n" + sums).encode()) + elif url == "https://static.rust-lang.org/rustup/release-stable.toml": + return io.BytesIO(b"schema-version = '1'\nversion = '9.9.9'\n") + elif url.endswith("/rustup-init.sha256"): + checksum = "" if unverified == "rustup-init" else "d" * 64 + return io.BytesIO(f"{checksum} *./rustup-init\n".encode()) + elif url == "https://pypi.org/pypi/psutil/json": + files = [] if unverified == "psutil" else [{"digests": {"sha256": "e" * 64}}] + body = {"info": {"version": "9.9.9"}, "urls": files} else: body = {"version": "18.9.9"} return io.BytesIO(json.dumps(body).encode()) @@ -140,7 +150,7 @@ def urlopen(request, **kwargs): def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeypatch): upstream(monkeypatch) latest = installer.resolve_latest("linux-x86_64") - for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse"): + for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse", "uv", "obscura"): assert latest[tool]["version"] == "9.9.9" assert latest[tool]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 assert latest["gh"]["assets"]["linux-x86_64"]["format"] == "tar" @@ -150,11 +160,15 @@ def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeyp assert latest["node"]["version"] == "30.1.0" assert node["url"] == "https://nodejs.org/dist/v30.1.0/node-v30.1.0-linux-x64.tar.xz" assert node["sha256"] == "b" * 64 - for tool in ("omp", "chrome-devtools-axi", "gh-axi", "lavish-axi", "quota-axi", "tasks-axi"): + rustup = latest["rustup-init"]["assets"]["linux-x86_64"] + assert rustup["url"].endswith("/9.9.9/x86_64-unknown-linux-gnu/rustup-init") + assert (rustup["sha256"], rustup["format"]) == ("d" * 64, "file") + assert latest["psutil"] == {"version": "9.9.9", "sha256": ["e" * 64]} + for tool in (*installer.NPM_LATEST, "supabase"): assert latest[tool] == "18.9.9" -@pytest.mark.parametrize("source", [*RELEASES, "node"]) +@pytest.mark.parametrize("source", [*RELEASES, "node", "rustup-init", "psutil"]) def test_release_without_a_published_checksum_is_refused(monkeypatch, source): upstream(monkeypatch, unverified=source) with pytest.raises(ValueError, match="refusing an unverified binary"): diff --git a/toolchain.lock.json b/toolchain.lock.json deleted file mode 100644 index 3dd0344..0000000 --- a/toolchain.lock.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "schema_version": 1, - "tools": { - "uv": { - "version": "0.12.5", - "assets": { - "linux-x86_64": { - "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-x86_64-unknown-linux-gnu.tar.gz", - "sha256": "68a509da24b06b4223a1c0175fb5eb5bc79342b76cbeff0cfe51ac3f5b17b6b2", - "format": "tar", - "binaries": { - "uv": "uv-x86_64-unknown-linux-gnu/uv", - "uvx": "uv-x86_64-unknown-linux-gnu/uvx" - } - }, - "linux-aarch64": { - "url": "https://github.com/astral-sh/uv/releases/download/0.12.5/uv-aarch64-unknown-linux-gnu.tar.gz", - "sha256": "9bf43b4d1a07665bf64d4c4e710930b382321a785e0eb10aac07f46471f86a31", - "format": "tar", - "binaries": { - "uv": "uv-aarch64-unknown-linux-gnu/uv", - "uvx": "uv-aarch64-unknown-linux-gnu/uvx" - } - } - } - }, - "rustup-init": { - "version": "1.29.0", - "assets": { - "linux-x86_64": { - "url": "https://static.rust-lang.org/rustup/archive/1.29.0/x86_64-unknown-linux-gnu/rustup-init", - "sha256": "4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10", - "format": "file", - "binaries": { - "rustup-init": "rustup-init" - } - }, - "linux-aarch64": { - "url": "https://static.rust-lang.org/rustup/archive/1.29.0/aarch64-unknown-linux-gnu/rustup-init", - "sha256": "9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792", - "format": "file", - "binaries": { - "rustup-init": "rustup-init" - } - } - } - } - }, - "rust_toolchain": "1.97.1" -} diff --git a/tools/npm/package-lock.json b/tools/npm/package-lock.json deleted file mode 100644 index e0f2e95..0000000 --- a/tools/npm/package-lock.json +++ /dev/null @@ -1,1057 +0,0 @@ -{ - "name": "code-factory-agent-tools", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "code-factory-agent-tools", - "version": "1.0.0", - "dependencies": { - "acpx": "0.18.0", - "chrome-devtools-mcp": "1.9.0" - } - }, - "node_modules/@agentclientprotocol/sdk": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.5.0.tgz", - "integrity": "sha512-524jwbB2iYWA+kWWyv9fhKbhU89dH/lu9u5EXwVNmfYzopV8BujCDxByBDZhxRUkB7RWIJzISCnwivdDk+bdVg==", - "license": "Apache-2.0", - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/@clack/core": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", - "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", - "license": "MIT", - "dependencies": { - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@clack/prompts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", - "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", - "license": "MIT", - "dependencies": { - "@clack/core": "1.5.1", - "fast-string-width": "^3.0.2", - "fast-wrap-ansi": "^0.2.0", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 20.12.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@openclaw/fs-safe": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.12.0.tgz", - "integrity": "sha512-Dwu2BZL6+n9pTeSLwijjL+dUBN0ktOjyucZPDjwSpuIkiPx9aussFWdyiWUD9UvNu3gwfLRAcktz1HfYT8QW1w==", - "license": "MIT", - "engines": { - "node": ">=22" - }, - "optionalDependencies": { - "@openclaw/fs-safe-darwin-arm64": "0.12.0", - "@openclaw/fs-safe-darwin-x64": "0.12.0", - "@openclaw/fs-safe-linux-arm64-gnu": "0.12.0", - "@openclaw/fs-safe-linux-arm64-musl": "0.12.0", - "@openclaw/fs-safe-linux-x64-gnu": "0.12.0", - "@openclaw/fs-safe-linux-x64-musl": "0.12.0", - "@openclaw/fs-safe-win32-x64-msvc": "0.12.0", - "jszip": "^3.10.2" - } - }, - "node_modules/@openclaw/fs-safe-darwin-arm64": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-arm64/-/fs-safe-darwin-arm64-0.12.0.tgz", - "integrity": "sha512-N0eX53qE+1zRErYTVeUz4/YBIZCvI4iNaFdCdny9BIc32UrnP1L/3Zj4+B0GSOGQFQBffDRd4M8orHjpiCgzIw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/@openclaw/fs-safe-darwin-x64": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-darwin-x64/-/fs-safe-darwin-x64-0.12.0.tgz", - "integrity": "sha512-az+rfw1exd1t0m3mJSSAqhExngWs5yT9igPewsOU59WvGsE13n2v0YrK1jtaYaBEebMYOdm6WnIFXuIq6KLHyQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/@openclaw/fs-safe-linux-arm64-gnu": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-gnu/-/fs-safe-linux-arm64-gnu-0.12.0.tgz", - "integrity": "sha512-ADBxjAz+Kjmfuv1tyH0B/pRBSlbymUpemCpc7MjzYwNF1lVunQKxaK5BM4e741Cz1Di6djKs/sIPag5TeL2Edw==", - "cpu": [ - "arm64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/@openclaw/fs-safe-linux-arm64-musl": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-arm64-musl/-/fs-safe-linux-arm64-musl-0.12.0.tgz", - "integrity": "sha512-Gs9xHGxwgEi2/yJgE9SO5qfxO55Ly+HDdjNyRLR5EAXZlh521b5mbh0ccNb5ivwT5X3MFOq2p0hSWdhW63fQpg==", - "cpu": [ - "arm64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/@openclaw/fs-safe-linux-x64-gnu": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-gnu/-/fs-safe-linux-x64-gnu-0.12.0.tgz", - "integrity": "sha512-xyrhaPVsCjIlbcOjjw99+7vDD23dO9J+uod2pN3W/J8dpLXiCcBsdK/L8QQ/106f9uF13jOFTRkIkmbPp+PuaA==", - "cpu": [ - "x64" - ], - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/@openclaw/fs-safe-linux-x64-musl": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-linux-x64-musl/-/fs-safe-linux-x64-musl-0.12.0.tgz", - "integrity": "sha512-7PN3Rg/76HFZoP/Pnr2SsWUAMjm1EB+41Rf0oVUGsrvey8gCAHixdt7d70FbODwj+xkd2mur9ed0HhN1INeY2g==", - "cpu": [ - "x64" - ], - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/@openclaw/fs-safe-win32-x64-msvc": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe-win32-x64-msvc/-/fs-safe-win32-x64-msvc-0.12.0.tgz", - "integrity": "sha512-hI+0jj3uwl6iWlOLu6bYAfaUzYwITIeM8z5P38mY59vnVppduBHyomKwsuO/aW6xsNKwR9rDH/FrsLbiTyZW/A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=22" - } - }, - "node_modules/acpx": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/acpx/-/acpx-0.18.0.tgz", - "integrity": "sha512-O5L2ldiSijytxsot9IIuViHCeZGNZpexlWKTD6za/flqrswotfl7RJ5Ch0NaU6ceSxFqAnLT/KVPwUF9KGVZlQ==", - "license": "MIT", - "dependencies": { - "@agentclientprotocol/sdk": "^1.4.0", - "@openclaw/fs-safe": "^0.12.0", - "commander": "^15.0.0", - "skillflag": "^0.2.1", - "tsx": "^4.23.13", - "zod": "^4.6.2" - }, - "bin": { - "acpx": "dist/cli.js" - }, - "engines": { - "node": ">=22.13.0" - } - }, - "node_modules/b4a": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", - "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, - "node_modules/bare-events": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", - "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.2.tgz", - "integrity": "sha512-+ZI68KHMUvosXfKbg/UOHK0tbCdRnegbvPEdEcZ3Nd6TetieQsJPRXBRXPdLyy8+3VSEbPXtsumTpEtt78xv9w==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.28.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-path": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", - "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", - "license": "Apache-2.0" - }, - "node_modules/bare-stream": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", - "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", - "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, - "node_modules/chrome-devtools-mcp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-1.9.0.tgz", - "integrity": "sha512-RnzXoJiUQ44hpOihWk90uOhLD/CnwDkDy0ldHMZONJ2nYQ+dWN1fq1luHHqyd+7FuYnyIlCY6uTThbN5ut9kSQ==", - "license": "Apache-2.0", - "bin": { - "chrome-devtools": "build/src/bin/chrome-devtools.js", - "chrome-devtools-mcp": "build/src/bin/chrome-devtools-mcp.js" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - }, - "peerDependencies": { - "@blackwell-systems/gcf": "^2.2.2", - "@toon-format/toon": "^4.1.0" - }, - "peerDependenciesMeta": { - "@blackwell-systems/gcf": { - "optional": true - }, - "@toon-format/toon": { - "optional": true - } - } - }, - "node_modules/commander": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", - "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT", - "optional": true - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, - "node_modules/fast-string-truncated-width": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", - "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", - "license": "MIT" - }, - "node_modules/fast-string-width": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", - "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", - "license": "MIT", - "dependencies": { - "fast-string-truncated-width": "^3.0.2" - } - }, - "node_modules/fast-wrap-ansi": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", - "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", - "license": "MIT", - "dependencies": { - "fast-string-width": "^3.0.2" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT", - "optional": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC", - "optional": true - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT", - "optional": true - }, - "node_modules/jszip": { - "version": "3.10.2", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", - "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", - "license": "(MIT OR GPL-3.0-or-later)", - "optional": true, - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)", - "optional": true - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT", - "optional": true - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "optional": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT", - "optional": true - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT", - "optional": true - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/skillflag": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/skillflag/-/skillflag-0.2.1.tgz", - "integrity": "sha512-47lfgUr6xzgljtTD5XfJrS+6muNb9R44OEr+o31Nco2R65W1xOA8Pi6QSbLa90tDYE6TqW5Hrh3N1egserGrhQ==", - "license": "MIT", - "dependencies": { - "@clack/prompts": "^1.0.1", - "tar-stream": "^3.1.7" - }, - "bin": { - "skill-install": "dist/bin/skill-install.js", - "skillflag": "dist/bin/skillflag.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/streamx": { - "version": "2.28.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", - "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "optional": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/tar-stream": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.1.tgz", - "integrity": "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, - "node_modules/tsx": { - "version": "4.23.15", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", - "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT", - "optional": true - }, - "node_modules/zod": { - "version": "4.6.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", - "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } -} diff --git a/tools/npm/package.json b/tools/npm/package.json deleted file mode 100644 index a35b5d7..0000000 --- a/tools/npm/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "code-factory-agent-tools", - "private": true, - "version": "1.0.0", - "description": "Pinned agent CLI tools; no credentials", - "dependencies": { - "acpx": "0.18.0", - "chrome-devtools-mcp": "1.9.0" - } -} From 8f099901fbb1d3947e5087c59e9099a4d854017c Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 00:28:47 +0000 Subject: [PATCH 04/13] ci: pin each action to its latest release SHA and let Dependabot move the pins astral-sh/setup-uv moves to v10.2.0; actions/checkout v7.0.1 and oven-sh/setup-bun v2.2.0 are already their latest releases. A weekly github-actions Dependabot update keeps the SHA pins on the newest release. --- .github/dependabot.yml | 7 +++++++ .github/workflows/ci.yml | 9 ++++----- CONTRIBUTING.md | 2 +- docs/architecture.md | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5046880 --- /dev/null +++ b/.github/dependabot.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bab5d8..df0ac03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,9 @@ # 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) +# 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 @@ -46,7 +45,7 @@ jobs: persist-credentials: false - name: Install latest uv - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: version: latest enable-cache: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a8e32fe..fbdf213 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,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. -Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts. Native assets are refused without a published SHA-256, npm tools install the exact resolved version, and the Rust toolchain follows `stable`. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. Only the repository's own Python environment (`uv.lock`) and the GitHub Actions in CI stay locked. +Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts. Native assets are refused without a published SHA-256, npm tools install the exact resolved version, and the Rust toolchain follows `stable`. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. 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. diff --git a/docs/architecture.md b/docs/architecture.md index 6e9756c..22f70bb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -55,7 +55,7 @@ Host sizing and every auto pruner are listed in [Capacity and pruners](capacity. 1. Everything latest, verified by published checksums. Each apply resolves every tool's newest release once (`scripts/install_tools.py --resolve`), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The Rust toolchain follows `stable`, omp plugins are upgraded in place, and the worker image builds `FROM ubuntu:latest`. 2. Do not copy a live global package directory. -3. Two locks stay, because they are this repository's own development environment rather than installed tools: change Python dependencies with `uv lock` and commit the lock, and keep GitHub Actions pinned to commit SHAs. +3. Two locks stay, because they are this repository's own development environment rather than installed tools: change Python dependencies with `uv lock` and commit the lock, and keep each GitHub Action pinned to the commit SHA of its latest release, which `.github/dependabot.yml` advances weekly. 4. Keep machine differences in ignored `.local/host.yml`; schema validation precedes provisioning. 5. Do not force, stash, reset, or overwrite a modified Firstmate checkout or an unmanaged command. Resolve that conflict explicitly. 6. Keep authentication and mutable application state outside the recipe. Provider model access must be checked on the destination account. @@ -69,7 +69,7 @@ GitHub Actions (`.github/workflows/ci.yml`) runs on pushes to `main`, on every p - Audits of the Dockerfile, devcontainer, and Compose definitions (the image builds from `ubuntu:latest`, Compose services are digest-pinned, no host namespaces or socket, resource caps). - A full worker image build and the behavior smoke in `tests/container-smoke.sh`. -Every action is pinned to an immutable commit SHA, and the token is read-only. uv and Bun are their latest releases, the same as `./bootstrap.sh` and `./factory apply` install. +Every action is pinned to the commit SHA of its latest release (Dependabot moves the pins weekly), and the token is read-only. uv and Bun are their latest releases, the same as `./bootstrap.sh` and `./factory apply` install. ## Primary sources From cb5e1c67ba2de1a842a8819a5045b65cca4e23f4 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 01:29:49 +0000 Subject: [PATCH 05/13] no-mistakes(review): Keep bun .env loading, resolve only installed tools --- Dockerfile | 10 +-- ansible/group_vars/all.yml | 20 +++++- ansible/tasks/preflight.yml | 9 ++- ansible/tasks/tools.yml | 4 +- ansible/templates/herdr.service.j2 | 2 +- docs/architecture.md | 2 +- docs/dependencies.md | 2 +- docs/fleet-guards.md | 2 +- docs/recovery.md | 2 +- fleet/doctor/worktree-env-seed.sh | 9 +-- scripts/install_tools.py | 104 +++++++++++++++++------------ tests/container-smoke.sh | 3 +- tests/test_install_tools.py | 58 +++++++++++++++- 13 files changed, 158 insertions(+), 69 deletions(-) diff --git a/Dockerfile b/Dockerfile index 04dbb9a..01426c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -137,8 +137,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 -# Latest 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}" @@ -153,8 +155,8 @@ RUN set -eux; uv run --project . --locked python containers/assert-image-config. # 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 this step only, never as an ARG, ENV, layer file or history entry; -# without it the lookup runs unauthenticated. +# 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}" diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index baa7235..817fbf5 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -57,11 +57,13 @@ factory_chrome_devtools_mcp_path: "{{ factory_share_dir }}/chrome-devtools-mcp/{ # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that # provides the MCP entrypoint is installed by `--npm`. With fleet guards, every -# bun loads the worktree's seeded .env.fleet heap cap (a missing file is a -# no-op; see fleet/doctor/worktree-env-seed.sh). +# bun also loads the worktree's seeded .env.fleet heap cap (a missing file is a +# no-op; see fleet/doctor/worktree-env-seed.sh). --env-file replaces bun's +# default .env/.env.local autoload, so those two are listed ahead of it; bun's +# NODE_ENV-specific .env. files are not loaded under fleet guards. factory_managed_shell_env: >- {{ {'CHROME_DEVTOOLS_AXI_MCP_PATH': factory_chrome_devtools_mcp_path} - | combine({'BUN_OPTIONS': '--env-file=.env.fleet'} + | combine({'BUN_OPTIONS': '--env-file=.env --env-file=.env.local --env-file=.env.fleet'} if (factory_cfg.profiles.fleet_guards | bool) else {}) }} # Contract: compute the unit ExecStart directly, never /usr/local/bin or $PATH. factory_herdr_bin: "{{ factory_tools_root }}/herdr/{{ factory_herdr_version }}/{{ factory_platform }}/herdr" @@ -179,6 +181,18 @@ factory_desktop_packages: # --- Tool installer --------------------------------------------------------- factory_core_tools: [herdr, node, bun, uv] +# The installer flags that choose which tools a run covers. tasks/preflight.yml +# resolves exactly this set (plus the sources Ansible installs itself) and +# tasks/tools.yml installs it, so a host never resolves, and never fails on, a +# release it does not install. +factory_installer_selection: >- + {{ ['--tools', factory_core_tools | join(',')] + + (['--npm'] if (factory_cfg.profiles.agents | bool) else []) + + (['--development'] if (factory_cfg.profiles.development | bool) else []) }} +factory_installer_also: >- + {{ (['psutil'] if ((factory_cfg.profiles.agents | bool) + and (factory_cfg.browser_prune.enabled | bool)) else []) + + (['obscura', 'supabase'] if (factory_cfg.profiles.fleet_guards | bool) else []) }} # --- Herdr static config ---------------------------------------------------- # Only the reviewed, non-secret subset is templated. experimental.pane_history diff --git a/ansible/tasks/preflight.yml b/ansible/tasks/preflight.yml index 0d48a6e..2e5f66e 100644 --- a/ansible/tasks/preflight.yml +++ b/ansible/tasks/preflight.yml @@ -84,12 +84,17 @@ quiet: true # Resolved once per run, in plan too, so a release landing mid-run cannot -# split what is installed from what the unit runs and verify asserts. The +# split what is installed from what the unit runs and verify asserts. Only the +# releases this host installs are resolved (factory_installer_selection and +# factory_installer_also), so a source it never uses cannot fail the run. The # installer refuses a native release whose publisher lists no SHA-256 for the # asset. - name: Resolve the latest tool releases ansible.builtin.command: - argv: [python3, "{{ factory_installer }}", --home, "{{ factory_cfg.home }}", --resolve] + argv: >- + {{ ['python3', factory_installer, '--home', factory_cfg.home, + '--also', factory_installer_also | join(','), '--resolve'] + + factory_installer_selection }} register: factory_latest_run changed_when: false check_mode: false diff --git a/ansible/tasks/tools.yml b/ansible/tasks/tools.yml index 52f01ef..a4bf115 100644 --- a/ansible/tasks/tools.yml +++ b/ansible/tasks/tools.yml @@ -14,10 +14,8 @@ factory_installer_argv: >- {{ ['python3', factory_installer, '--home', factory_cfg.home, - '--tools', factory_core_tools | join(','), '--resolved', factory_latest | to_json] - + (['--npm'] if (factory_cfg.profiles.agents | bool) else []) - + (['--development'] if (factory_cfg.profiles.development | bool) else []) }} + + factory_installer_selection }} - name: Show the installer invocation ansible.builtin.debug: diff --git a/ansible/templates/herdr.service.j2 b/ansible/templates/herdr.service.j2 index 2bc5203..8d68c77 100644 --- a/ansible/templates/herdr.service.j2 +++ b/ansible/templates/herdr.service.j2 @@ -17,7 +17,7 @@ Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin # The installed MCP entrypoint, so AXI never resolves a floating npx package, and # with fleet guards the bun heap-cap env file. {% for key, value in factory_managed_shell_env | dictsort %} -Environment={{ key }}={{ value }} +Environment="{{ key }}={{ value }}" {% endfor %} {% endif %} Restart=on-failure diff --git a/docs/architecture.md b/docs/architecture.md index 22f70bb..fcf1fdb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -53,7 +53,7 @@ Host sizing and every auto pruner are listed in [Capacity and pruners](capacity. ## Reproducibility policy -1. Everything latest, verified by published checksums. Each apply resolves every tool's newest release once (`scripts/install_tools.py --resolve`), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The Rust toolchain follows `stable`, omp plugins are upgraded in place, and the worker image builds `FROM ubuntu:latest`. +1. Everything latest, verified by published checksums. Each apply resolves the newest release of every tool the host installs, once (`scripts/install_tools.py --resolve`, with the same `--tools`, `--npm` and `--development` selection as the install), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The Rust toolchain follows `stable`, omp plugins are upgraded in place, and the worker image builds `FROM ubuntu:latest`. 2. Do not copy a live global package directory. 3. Two locks stay, because they are this repository's own development environment rather than installed tools: change Python dependencies with `uv lock` and commit the lock, and keep each GitHub Action pinned to the commit SHA of its latest release, which `.github/dependabot.yml` advances weekly. 4. Keep machine differences in ignored `.local/host.yml`; schema validation precedes provisioning. diff --git a/docs/dependencies.md b/docs/dependencies.md index e5233cd..ea934b1 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -20,7 +20,7 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne - `agents` profile, omp plugins: ponytail, i-have-adhd and caveman from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. - `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. -The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); one resolution makes seven. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. +The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most seven. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. ## Ubuntu packages diff --git a/docs/fleet-guards.md b/docs/fleet-guards.md index e983beb..dd6bd31 100644 --- a/docs/fleet-guards.md +++ b/docs/fleet-guards.md @@ -29,7 +29,7 @@ Sizing per lane count and the full list of pruners are in | `oss-fleet/doctor/dev-server-reaper.sh` | Every 2 minutes: kills `next dev`/`next-server`/`tsc --noEmit` trees in treehouse worktrees whose lane last reported `done:`/`paused:`/`blocked:`/`failed:`, has no agent process, or whose agent transcript is idle >= 30 min (`REAPER_IDLE_MIN`). A dev server is 3-4 GB and restarts in 10 s; idle ones from finished lanes are what filled swap. One `next dev` per branch is inherent - Next compiles the whole app per process - so the fix is lifetime, not sharing. | | `oss-fleet/doctor/storage-guard.sh` | Every 5 minutes: use% of the filesystems holding `/`, `/var/log`, the home and Docker's data root. WARN (85%) alerts once per episode, CRIT (92%) prunes only regenerable Docker data, and a fill rate projecting the disk full within 6 hours alerts even below WARN. See [Storage guard](#storage-guard). | | `oss-fleet/doctor/devtools-bridge-reaper.sh` | Every 10 minutes: stops attached chrome-devtools-axi bridges (`CHROME_DEVTOOLS_AXI_BROWSER_URL` set) whose process tree used no CPU and whose session state files did not change for 60 min (`REAPER_IDLE_MIN`). See [Devtools-bridge reaper](#devtools-bridge-reaper). | -| worktree `.env.fleet` (seeded, git-excluded) | `NODE_OPTIONS=--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`). The managed shell and Herdr environment set `BUN_OPTIONS=--env-file=.env.fleet`, so every bun started in the worktree root (`bun run dev`, `bun run tsc`, and omp itself) passes the cap to the node processes it spawns, and a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. Bun started outside the worktree root finds no such file and runs uncapped. Hidden through the shared `.git/info/exclude`; never written when the repository tracks its own `.env.fleet`. | +| worktree `.env.fleet` (seeded, git-excluded) | `NODE_OPTIONS=--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`). The managed shell and Herdr environment set `BUN_OPTIONS="--env-file=.env --env-file=.env.local --env-file=.env.fleet"` (an explicit `--env-file` replaces bun's default `.env` and `.env.local` autoload, so both are listed; the `NODE_ENV`-specific `.env.` files are not loaded), so every bun started in the worktree root (`bun run dev`, `bun run tsc`, and omp itself) passes the cap to the node processes it spawns, and a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. Bun started outside the worktree root finds no `.env.fleet` and runs uncapped. Hidden through the shared `.git/info/exclude`; never written when the repository tracks its own `.env.fleet`. | | `.local/bin/supabase` | Shim: `status`/`--version` pass through; every lifecycle or schema subcommand is refused with the reason. `npx supabase` bypasses it, which is why the Docker guard exists. | | `.config/systemd/user/flotilla-*.{service,timer,path}` | Login start + 5-minute keeper for the stack; the guard as a restart-always service; the seeder on pool changes, every 2 minutes and at login. | | `/etc/docker/daemon.json` | `init: true` and `live-restore: true` merged in (tasks/docker.yml, any profile with docker). | diff --git a/docs/recovery.md b/docs/recovery.md index ab47b48..1b0cc0e 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -56,7 +56,7 @@ Never upload a raw failed HTTP request to a public issue or this repository. Suc ## Drift and upgrades -Run `./factory plan` before applying changed pins. Nothing is pinned: every apply upgrades every tool to its latest release, and a herdr upgrade restarts `herdr.service`. An unmanaged executable at a managed command path is a refusal, not permission to overwrite it. Firstmate refuses dirty or independently advanced checkouts. Preserve that work and decide whether to update the configuration's pin or move to a separate clean checkout. +Run `./factory plan` before applying; every apply upgrades each tool to its latest release, and a herdr upgrade restarts `herdr.service`. An unmanaged executable at a managed command path is a refusal, not permission to overwrite it. Firstmate refuses dirty or independently advanced checkouts. Preserve that work and decide whether to update the configuration or move to a separate clean checkout. Existing OMP settings are first-write-only: provisioning will not replace provider configuration or credentials on a reused account. Review and merge the exported safe preferences manually if deliberately updating an established account. diff --git a/fleet/doctor/worktree-env-seed.sh b/fleet/doctor/worktree-env-seed.sh index 6e92219..533910c 100755 --- a/fleet/doctor/worktree-env-seed.sh +++ b/fleet/doctor/worktree-env-seed.sh @@ -33,10 +33,11 @@ seed_one() { } # Heap cap for everything bun runs in the worktree (bun run dev, tsc): the -# managed shell and Herdr environment set BUN_OPTIONS=--env-file=.env.fleet, so -# every bun started in the worktree root loads this file and hands NODE_OPTIONS -# to the node processes it spawns. A runaway compile then fails fast with a -# heap error the agent can see, instead of creeping to 4 GB and swapping the +# managed shell and Herdr environment set BUN_OPTIONS to load .env, .env.local +# and .env.fleet, so every bun started in the worktree root loads this file +# and hands NODE_OPTIONS to the node processes it spawns. A runaway compile +# then fails fast with a heap error the agent can see, instead of creeping to +# 4 GB and swapping the # host. The file is hidden from git through the shared .git/info/exclude, so no # lane ever sees or commits it. HEAP_MB=${FLEET_NODE_HEAP_MB:-2048} diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 57789c7..a074956 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -128,10 +128,12 @@ def verified(tool, version, key, name, url, checksum, binaries): return {"version": version, "assets": {key: asset}} -def resolve_latest(key): - """Specs for the newest native releases, and the newest npm tool versions.""" +def resolve_latest(key, names): + """Specs for the newest releases of exactly these tools, and nothing else.""" latest = {} for tool, (repo, prefix, pattern, binaries) in GITHUB_LATEST.items(): + if tool not in names: + continue release = json.loads(fetch(GITHUB_API.format(repo), f"{tool} release")) version = release["tag_name"].removeprefix(prefix) name = pattern.format(v=version, key=key, **ARCH[key]) @@ -139,44 +141,51 @@ def resolve_latest(key): checksum = (asset.get("digest") or "").removeprefix("sha256:") url = asset.get("browser_download_url", "") latest[tool] = verified(tool, version, key, name, url, checksum, binaries) - releases = json.loads(fetch("https://nodejs.org/dist/index.json", "node release")) - tag = max((r["version"] for r in releases), key=lambda v: tuple(map(int, v[1:].split(".")))) - name = f"node-{tag}-linux-{ARCH[key]['node']}.tar.xz" - sums = fetch(f"https://nodejs.org/dist/{tag}/SHASUMS256.txt", "node checksums").decode() - match = re.search(rf"^([0-9a-f]{{64}}) {re.escape(name)}$", sums, re.M) - binaries = {command: f"node-v*/bin/{command}" for command in ("node", "npm", "npx")} - latest["node"] = verified( - "node", - tag.removeprefix("v"), - key, - name, - f"https://nodejs.org/dist/{tag}/{name}", - match[1] if match else "", - binaries, - ) - toml = fetch("https://static.rust-lang.org/rustup/release-stable.toml", "rustup").decode() - match = re.search(r"^version = '([0-9.]+)'$", toml, re.M) - if not match: - raise ValueError("cannot read the latest rustup version") - version = match[1] - url = f"https://static.rust-lang.org/rustup/archive/{version}/{ARCH[key]['gnu']}-unknown-linux-gnu/rustup-init" - checksum = fetch(url + ".sha256", "rustup-init checksum").decode().split(" ")[0] - binaries = {"rustup-init": "rustup-init"} - latest["rustup-init"] = verified( - "rustup-init", version, key, "rustup-init", url, checksum, binaries - ) + if "node" in names: + releases = json.loads(fetch("https://nodejs.org/dist/index.json", "node release")) + tag = max((r["version"] for r in releases), key=lambda v: tuple(map(int, v[1:].split(".")))) + name = f"node-{tag}-linux-{ARCH[key]['node']}.tar.xz" + sums = fetch(f"https://nodejs.org/dist/{tag}/SHASUMS256.txt", "node checksums").decode() + match = re.search(rf"^([0-9a-f]{{64}}) {re.escape(name)}$", sums, re.M) + binaries = {command: f"node-v*/bin/{command}" for command in ("node", "npm", "npx")} + latest["node"] = verified( + "node", + tag.removeprefix("v"), + key, + name, + f"https://nodejs.org/dist/{tag}/{name}", + match[1] if match else "", + binaries, + ) + if "rustup-init" in names: + toml = fetch("https://static.rust-lang.org/rustup/release-stable.toml", "rustup").decode() + match = re.search(r"^version = '([0-9.]+)'$", toml, re.M) + if not match: + raise ValueError("cannot read the latest rustup version") + version = match[1] + url = f"https://static.rust-lang.org/rustup/archive/{version}/{ARCH[key]['gnu']}-unknown-linux-gnu/rustup-init" + checksum = fetch(url + ".sha256", "rustup-init checksum").decode().split(" ")[0] + binaries = {"rustup-init": "rustup-init"} + latest["rustup-init"] = verified( + "rustup-init", version, key, "rustup-init", url, checksum, binaries + ) for tool, package in NPM_LATEST.items(): - registry = fetch(f"https://registry.npmjs.org/{package}/latest", f"{tool} version") - latest[tool] = json.loads(registry)["version"] - # Resolved for the fleet guards' shared Supabase stack, installed by Ansible. - registry = fetch("https://registry.npmjs.org/supabase/latest", "supabase version") - latest["supabase"] = json.loads(registry)["version"] - # The Chrome pruner's runtime, installed by Ansible with uv --require-hashes. - pypi = json.loads(fetch("https://pypi.org/pypi/psutil/json", "psutil release")) - hashes = [file["digests"]["sha256"] for file in pypi["urls"]] - if not hashes or not all(re.fullmatch(r"[0-9a-f]{64}", h) for h in hashes): - raise ValueError("psutil publishes no SHA-256 digests; refusing an unverified binary") - latest["psutil"] = {"version": pypi["info"]["version"], "sha256": hashes} + if tool in names: + registry = fetch(f"https://registry.npmjs.org/{package}/latest", f"{tool} version") + latest[tool] = json.loads(registry)["version"] + if "supabase" in names: + # Resolved for the fleet guards' shared Supabase stack, installed by Ansible. + registry = fetch("https://registry.npmjs.org/supabase/latest", "supabase version") + latest["supabase"] = json.loads(registry)["version"] + if "psutil" in names: + # The Chrome pruner's runtime, installed by Ansible with uv --require-hashes. + pypi = json.loads(fetch("https://pypi.org/pypi/psutil/json", "psutil release")) + hashes = [file["digests"]["sha256"] for file in pypi["urls"]] + if not hashes or not all(re.fullmatch(r"[0-9a-f]{64}", h) for h in hashes): + raise ValueError("psutil publishes no SHA-256 digests; refusing an unverified binary") + latest["psutil"] = {"version": pypi["info"]["version"], "sha256": hashes} + if unknown := sorted(set(names) - latest.keys()): + raise ValueError(f"no latest release source for: {', '.join(unknown)}") return latest @@ -465,6 +474,12 @@ def main(): parser.add_argument("--tools", default="herdr,node,bun,uv") parser.add_argument("--npm", action="store_true") parser.add_argument("--development", action="store_true") + parser.add_argument( + "--also", + default="", + help="comma-separated extra sources to resolve that Ansible installs itself: " + "obscura, supabase, psutil", + ) parser.add_argument( "--resolve", action="store_true", help="print the latest releases as JSON and exit" ) @@ -473,16 +488,17 @@ def main(): ) args = parser.parse_args() key = platform_key() + names = list(dict.fromkeys(args.tools.split(",") + (AGENT_TOOLS if args.npm else []))) + if args.development: + names.append("rustup-init") + sources = {*names, *(NPM_LATEST if args.npm else ()), *filter(None, args.also.split(","))} if args.resolve: - print(json.dumps(resolve_latest(key))) + print(json.dumps(resolve_latest(key, sources))) return home = args.home.resolve(strict=True) if home.stat().st_uid != os.geteuid(): parser.error("run as the user who owns --home") - names = list(dict.fromkeys(args.tools.split(",") + (AGENT_TOOLS if args.npm else []))) - if args.development: - names.append("rustup-init") - latest = args.resolved or resolve_latest(key) + latest = args.resolved or resolve_latest(key, sources) environment = { **os.environ, "HOME": str(home), diff --git a/tests/container-smoke.sh b/tests/container-smoke.sh index dd0b758..d9a0f12 100755 --- a/tests/container-smoke.sh +++ b/tests/container-smoke.sh @@ -228,7 +228,8 @@ herdr_installed_bin() { image_is_current() { local latest versions='map_values(.version? // .)' latest=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ - --home "${HOME}" --resolve) || fail "could not resolve the latest releases" + --home "${HOME}" --tools herdr,node,bun,uv --npm --development --resolve) \ + || fail "could not resolve the latest releases" [ "$(jq -cS "${versions}" <<<"${latest}")" = "$(jq -cS "${versions}" "${RESOLVED_STAMP}")" ] \ && return 0 printf 'image has %s but upstream now has %s; idempotence not measured\n' \ diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 929efa9..157da90 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -147,9 +147,19 @@ def urlopen(request, **kwargs): monkeypatch.setattr(installer.urllib.request, "urlopen", urlopen) +EVERYTHING = { + *installer.GITHUB_LATEST, + *installer.NPM_LATEST, + "node", + "rustup-init", + "supabase", + "psutil", +} + + def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeypatch): upstream(monkeypatch) - latest = installer.resolve_latest("linux-x86_64") + latest = installer.resolve_latest("linux-x86_64", EVERYTHING) for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse", "uv", "obscura"): assert latest[tool]["version"] == "9.9.9" assert latest[tool]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 @@ -172,7 +182,7 @@ def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeyp def test_release_without_a_published_checksum_is_refused(monkeypatch, source): upstream(monkeypatch, unverified=source) with pytest.raises(ValueError, match="refusing an unverified binary"): - installer.resolve_latest("linux-x86_64") + installer.resolve_latest("linux-x86_64", EVERYTHING) @pytest.mark.parametrize(("env", "expected"), [("env-token", "Bearer env-token"), ("", None)]) @@ -180,12 +190,54 @@ def test_github_token_goes_only_to_the_github_api(monkeypatch, env, expected): seen = [] monkeypatch.setenv("GITHUB_TOKEN", env) upstream(monkeypatch, seen=seen) - installer.resolve_latest("linux-x86_64") + installer.resolve_latest("linux-x86_64", EVERYTHING) github = {auth for url, auth in seen if url.startswith("https://api.github.com/")} assert github == {expected} assert {auth for url, auth in seen if not url.startswith("https://api.github.com/")} == {None} +def run_cli(monkeypatch, capsys, tmp_path, *argv): + monkeypatch.setattr(installer, "platform_key", lambda: "linux-x86_64") + monkeypatch.setattr( + installer.sys, "argv", ["install_tools.py", "--home", str(tmp_path), *argv, "--resolve"] + ) + installer.main() + return json.loads(capsys.readouterr().out) + + +def test_resolve_covers_only_the_requested_tools(monkeypatch, capsys, tmp_path): + seen = [] + upstream(monkeypatch, unverified="psutil", seen=seen) + latest = run_cli(monkeypatch, capsys, tmp_path, "--tools", "uv") + assert set(latest) == {"uv"} + assert [url for url, _ in seen] == ["https://api.github.com/repos/astral-sh/uv/releases/latest"] + + +@pytest.mark.parametrize( + ("argv", "expected"), + [ + (["--tools", "herdr,node"], {"herdr", "node"}), + (["--tools", "uv", "--development"], {"uv", "rustup-init"}), + ( + ["--tools", "uv", "--npm"], + {"uv", *installer.AGENT_TOOLS, *installer.NPM_LATEST}, + ), + (["--tools", "uv", "--also", "psutil,supabase"], {"uv", "psutil", "supabase"}), + ], +) +def test_resolve_selection_matches_what_the_flags_install( + monkeypatch, capsys, tmp_path, argv, expected +): + upstream(monkeypatch) + assert set(run_cli(monkeypatch, capsys, tmp_path, *argv)) == expected + + +def test_resolve_refuses_a_source_that_does_not_exist(monkeypatch, capsys, tmp_path): + upstream(monkeypatch) + with pytest.raises(ValueError, match="no latest release source for: nonesuch"): + run_cli(monkeypatch, capsys, tmp_path, "--tools", "uv", "--also", "nonesuch") + + def test_commands_of_removed_packages_are_unlinked_but_user_links_kept(tmp_path): bin_dir = tmp_path / ".local/bin" bin_dir.mkdir(parents=True) From 33585f78ceebe64e48fb6ed918089bf957e7ca51 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 01:41:33 +0000 Subject: [PATCH 06/13] no-mistakes(review): Keep resolved.json on bootstrap; show subprocess stderr --- scripts/install_tools.py | 8 ++++--- tests/test_install_tools.py | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/scripts/install_tools.py b/scripts/install_tools.py index a074956..e2f7b28 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -518,9 +518,10 @@ def main(): changed = omp_plugins(home, environment) or changed if args.development: changed = rust_install(home, environment) or changed - # The record checks compare against, so a later upstream release cannot - # make an unchanged install look wrong. - (prefix / "resolved.json").write_text(json.dumps(latest, sort_keys=True) + "\n") + if args.resolved: + # The record checks compare against, so a later upstream release cannot + # make an unchanged install look wrong. + (prefix / "resolved.json").write_text(json.dumps(latest, sort_keys=True) + "\n") print( json.dumps( { @@ -538,4 +539,5 @@ def main(): main() except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error: print(f"install_tools: {error}", file=sys.stderr) + print(getattr(error, "stderr", None) or "", end="", file=sys.stderr) sys.exit(1) diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 157da90..aa98741 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -2,6 +2,7 @@ import importlib.util import io import json +import runpy import subprocess import tarfile import zipfile @@ -248,3 +249,46 @@ def test_commands_of_removed_packages_are_unlinked_but_user_links_kept(tmp_path) assert installer.prune_dangling_links(tmp_path) assert not managed.is_symlink() assert user.is_symlink() + + +@pytest.mark.parametrize( + ("argv", "expected"), + [([], {"herdr": {}}), (["--resolved", json.dumps({"uv": {}})], {"uv": {}})], +) +def test_only_an_apply_with_a_resolved_record_rewrites_it(monkeypatch, tmp_path, argv, expected): + record = tmp_path / ".local/share/code-factory/resolved.json" + record.parent.mkdir(parents=True) + record.write_text(json.dumps({"herdr": {}}) + "\n") + upstream(monkeypatch) + monkeypatch.setattr(installer, "platform_key", lambda: "linux-x86_64") + monkeypatch.setattr(installer, "install_asset", lambda *args: False) + monkeypatch.setattr( + installer.sys, "argv", ["install_tools.py", "--home", str(tmp_path), "--tools", "uv", *argv] + ) + installer.main() + assert json.loads(record.read_text()) == expected + + +def test_a_failed_command_reports_why_it_failed(monkeypatch, capsys, tmp_path): + rustup = tmp_path / ".cargo/bin/rustup" + rustup.parent.mkdir(parents=True) + rustup.write_text("#!/bin/sh\necho 'error: network down' >&2\nexit 1\n") + rustup.chmod(0o755) + payload = b"#!/bin/sh\n" + resolved = {} + for name in ("uv", "rustup-init"): + resolved[name] = asset(payload) + resolved[name]["assets"]["linux-x86_64"]["binaries"] = {name: name} + monkeypatch.setattr(installer.platform, "system", lambda: "Linux") + monkeypatch.setattr(installer.platform, "machine", lambda: "x86_64") + monkeypatch.setattr(installer.urllib.request, "urlopen", lambda *a, **k: Download(payload)) + monkeypatch.setattr( + installer.sys, + "argv", + ["install_tools.py", "--home", str(tmp_path), "--tools", "uv", "--development"] + + ["--resolved", json.dumps(resolved)], + ) + with pytest.raises(SystemExit) as exit_status: + runpy.run_path(str(SPEC.origin), run_name="__main__") + assert exit_status.value.code == 1 + assert "error: network down" in capsys.readouterr().err From 7cb98396dc19f90e0f0c913f50421ee41086bae6 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 02:21:48 +0000 Subject: [PATCH 07/13] no-mistakes(review): Retire codex/pnpm, NODE_OPTIONS heap cap, merge resolved record --- CONTRIBUTING.md | 4 +- ansible/group_vars/all.yml | 15 ++-- ansible/tasks/agents.yml | 22 ++++++ ansible/tasks/fleet_guards.yml | 3 +- ansible/templates/herdr.service.j2 | 2 +- docs/architecture.md | 4 +- docs/capacity.md | 2 +- docs/dependencies.md | 5 +- docs/fleet-guards.md | 2 +- docs/security.md | 2 +- fleet/doctor/worktree-env-seed.sh | 36 --------- scripts/install_tools.py | 32 +++++--- tests/test_configuration.py | 123 ++++++++++++++++++++++++++++- tests/test_install_tools.py | 39 ++++++--- 14 files changed, 211 insertions(+), 80 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbdf213..cda08d8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +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` — checksum-verified tool installer (idempotent) +- `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 @@ -20,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. -Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts. Native assets are refused without a published SHA-256, npm tools install the exact resolved version, and the Rust toolchain follows `stable`. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. 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. +Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts, except the three omp marketplace plugins, which no publisher checksums and which track each author's default branch. Native assets are refused without a published SHA-256, npm tools install the exact resolved version, and the Rust toolchain follows `stable`. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. 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. diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 817fbf5..a5e6acd 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -56,14 +56,14 @@ factory_omp_root: "{{ factory_share_dir }}/omp/{{ factory_omp_version }}" factory_chrome_devtools_mcp_path: "{{ factory_share_dir }}/chrome-devtools-mcp/{{ factory_latest['chrome-devtools-mcp'] }}/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that -# provides the MCP entrypoint is installed by `--npm`. With fleet guards, every -# bun also loads the worktree's seeded .env.fleet heap cap (a missing file is a -# no-op; see fleet/doctor/worktree-env-seed.sh). --env-file replaces bun's -# default .env/.env.local autoload, so those two are listed ahead of it; bun's -# NODE_ENV-specific .env. files are not loaded under fleet guards. +# provides the MCP entrypoint is installed by `--npm`. With fleet guards, +# NODE_OPTIONS caps the heap of every node process the shell or Herdr starts +# (bun run dev, tsc), so a runaway compile fails fast instead of swapping the +# host. bun keeps its own .env, .env. and .env.local autoload. +factory_fleet_node_heap_mb: 2048 factory_managed_shell_env: >- {{ {'CHROME_DEVTOOLS_AXI_MCP_PATH': factory_chrome_devtools_mcp_path} - | combine({'BUN_OPTIONS': '--env-file=.env --env-file=.env.local --env-file=.env.fleet'} + | combine({'NODE_OPTIONS': '--max-old-space-size=' ~ factory_fleet_node_heap_mb} if (factory_cfg.profiles.fleet_guards | bool) else {}) }} # Contract: compute the unit ExecStart directly, never /usr/local/bin or $PATH. factory_herdr_bin: "{{ factory_tools_root }}/herdr/{{ factory_herdr_version }}/{{ factory_platform }}/herdr" @@ -222,6 +222,7 @@ factory_prune_timeout_seconds: 90 # first_write: true means the file is written only when it does not exist, so # an account that already holds provider settings is never overwritten. factory_omp_config: "{{ factory_cfg.home }}/.omp/agent/config.yml" +factory_acpx_config: "{{ factory_cfg.home }}/.acpx/config.json" factory_agent_config_files: - src: config/omp.yml dest: "{{ factory_omp_config }}" @@ -242,7 +243,7 @@ factory_agent_config_files: mode: "0600" first_write: true - src: config/acpx.json - dest: "{{ factory_cfg.home }}/.acpx/config.json" + dest: "{{ factory_acpx_config }}" dir_mode: "0700" mode: "0600" first_write: true diff --git a/ansible/tasks/agents.yml b/ansible/tasks/agents.yml index f946611..05dc947 100644 --- a/ansible/tasks/agents.yml +++ b/ansible/tasks/agents.yml @@ -129,3 +129,25 @@ - factory_omp_config_raw.content is defined - (current.statusLine | default({})).preset | default('default') == 'custom' - wanted != current + +# The acpx config is first-write-only, but codex is no longer installed, so a +# host written before that still names a missing agent as its default. Only +# that one value is rewritten, and only while it is still codex. +- name: Read the acpx config for the default agent + ansible.builtin.slurp: + path: "{{ factory_acpx_config }}" + become: true + register: factory_acpx_config_raw + failed_when: false + check_mode: false + +- name: Point the acpx default agent at omp when it is still codex + vars: + current: "{{ factory_acpx_config_raw.content | b64decode | from_json }}" + ansible.builtin.copy: + content: "{{ current | combine({'defaultAgent': 'omp'}) | to_nice_json(indent=2, sort_keys=false) }}\n" + dest: "{{ factory_acpx_config }}" + become: true + when: + - factory_acpx_config_raw.content is defined + - current.defaultAgent | default('') == 'codex' diff --git a/ansible/tasks/fleet_guards.yml b/ansible/tasks/fleet_guards.yml index d0befc5..fa16480 100644 --- a/ansible/tasks/fleet_guards.yml +++ b/ansible/tasks/fleet_guards.yml @@ -9,8 +9,7 @@ # any Supabase CLI project other than the shared one on creation and alerts # on bare Postgres containers. Allowlist is first-write (operator-owned). # * ~/oss-fleet/doctor/worktree-env-seed.sh - installs the stack's env file -# as .env.local in every swarms worktree, plus a git-excluded .env.fleet -# heap cap for bun-run scripts; path unit + timer + login. +# as .env.local in every swarms worktree; path unit + timer + login. # * ~/oss-fleet/doctor/dev-server-reaper.sh - every 2 min, kills next dev / # tsc trees in worktrees whose lane is done/paused/blocked, agent-less or # idle >= 30 min. Dev servers are throwaway; idle ones filled swap. diff --git a/ansible/templates/herdr.service.j2 b/ansible/templates/herdr.service.j2 index 8d68c77..8a71b6a 100644 --- a/ansible/templates/herdr.service.j2 +++ b/ansible/templates/herdr.service.j2 @@ -15,7 +15,7 @@ WorkingDirectory={{ factory_cfg.workspace }} Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin {% if factory_cfg.profiles.agents | bool %} # The installed MCP entrypoint, so AXI never resolves a floating npx package, and -# with fleet guards the bun heap-cap env file. +# with fleet guards the node heap cap (NODE_OPTIONS). {% for key, value in factory_managed_shell_env | dictsort %} Environment="{{ key }}={{ value }}" {% endfor %} diff --git a/docs/architecture.md b/docs/architecture.md index fcf1fdb..c089f41 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -15,7 +15,7 @@ The source machine already uses Ubuntu packages, user-level systemd services, ho | OpenTofu | Cloud instances, networks, DNS, resource lifecycle | Add when a provider/resource contract is chosen; no pretend provider configuration is shipped | | cloud-init | Initial VM prerequisites before configuration management | Small vendor-neutral bootstrap input only | -This is repeatable configuration, not a bit-identical OS image. Ubuntu packages receive distribution security updates. Everything the recipe installs is latest, verified by published checksums, so a later rebuild installs newer versions. Rebuilding an environment does not recreate authenticated accounts, databases, or running processes. +This is repeatable configuration, not a bit-identical OS image. Ubuntu packages receive distribution security updates. Everything the recipe installs is latest, verified by published checksums (the three omp marketplace plugins are the one exception: no publisher checksums them), so a later rebuild installs newer versions. Rebuilding an environment does not recreate authenticated accounts, databases, or running processes. ## Host and container boundary @@ -53,7 +53,7 @@ Host sizing and every auto pruner are listed in [Capacity and pruners](capacity. ## Reproducibility policy -1. Everything latest, verified by published checksums. Each apply resolves the newest release of every tool the host installs, once (`scripts/install_tools.py --resolve`, with the same `--tools`, `--npm` and `--development` selection as the install), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The Rust toolchain follows `stable`, omp plugins are upgraded in place, and the worker image builds `FROM ubuntu:latest`. +1. Everything latest, verified by published checksums. Each apply resolves the newest release of every tool the host installs, once (`scripts/install_tools.py --resolve`, with the same `--tools`, `--npm` and `--development` selection as the install), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The one exception is the omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, so each apply upgrades them in place to the latest commit of the author's default branch. The Rust toolchain follows `stable`, and the worker image builds `FROM ubuntu:latest`. 2. Do not copy a live global package directory. 3. Two locks stay, because they are this repository's own development environment rather than installed tools: change Python dependencies with `uv lock` and commit the lock, and keep each GitHub Action pinned to the commit SHA of its latest release, which `.github/dependabot.yml` advances weekly. 4. Keep machine differences in ignored `.local/host.yml`; schema validation precedes provisioning. diff --git a/docs/capacity.md b/docs/capacity.md index a08dd72..4b50a31 100644 --- a/docs/capacity.md +++ b/docs/capacity.md @@ -12,7 +12,7 @@ Figures are estimates from a measured fleet host. | omp crewmate (one agent process) | 300-750 MB RSS | measured | | no-mistakes pipeline agent (`omp acp`) while a lane ships | about 450 MB | measured | | `next dev` / `next-server` for a UI lane | 3-4 GB | [fleet guards](fleet-guards.md) | -| `tsc --noEmit` | about 0.6 GB; bun-run scripts capped at a 2048 MB heap | measured; seeded `.env.fleet` | +| `tsc --noEmit` | about 0.6 GB; node processes capped at a 2048 MB heap | measured; `NODE_OPTIONS` in the managed environment | | chrome-devtools-axi bridge + `chrome-devtools-mcp` | about 0.3 GB for the MCP child; an idle bridge holds about 2 GB | measured | | Obscura browser tier | about 25 MB idle | [browser ladder](fleet-guards.md#browser-ladder) | | **Light lane** (docs, config, backend: agent + pipeline + tests) | **about 1.35-1.8 GB**; plan 2 GB | sum of the agent, pipeline and `tsc` rows | diff --git a/docs/dependencies.md b/docs/dependencies.md index ea934b1..ac22725 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -1,6 +1,6 @@ # Dependencies -Everything the recipe installs, grouped by where it comes from. Nothing is pinned: every `./factory apply` resolves each tool's newest release once and installs exactly that, so re-running apply upgrades an existing host. Every download is verified against the checksum its publisher posts for that exact release, and a release without one fails the apply instead of installing an unverified artifact. The installer records the releases it resolved in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. `./factory plan` installs none of this. +Everything the recipe installs, grouped by where it comes from. Nothing is pinned: every `./factory apply` resolves each tool's newest release once and installs exactly that, so re-running apply upgrades an existing host. Every download is verified against the checksum its publisher posts for that exact release, and a release without one fails the apply instead of installing an unverified artifact. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them. The installer records the releases it resolved in `~/.local/share/code-factory/resolved.json`, which the container smoke compares against; `ansible/tasks/verify.yml` also asserts that the herdr service and omp run the resolved releases. `./factory plan` installs none of this. ## Repository tooling @@ -17,7 +17,8 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne - Always: node, the newest release in the [nodejs.org index](https://nodejs.org/dist/index.json) (not the LTS line), verified against that release's `SHASUMS256.txt`. - `agents` profile, native: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. - `agents` profile, npm: omp (`@oh-my-pi/pi-coding-agent`), chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, tasks-axi, acpx (runs the no-mistakes gate agent `acp:omp`), and chrome-devtools-mcp (the MCP build chrome-devtools-axi launches through `CHROME_DEVTOOLS_AXI_MCP_PATH`). The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. -- `agents` profile, omp plugins: ponytail, i-have-adhd and caveman from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. +- Retired: codex and pnpm are no longer installed (bun is the single package manager and runner; omp is the pi agent). An apply on a host that still has them removes the `codex`, `pnpm` and `pnpx` links in `~/.local/bin` that point into the old `~/.local/share/code-factory/npm` prefix, deletes that prefix, and changes `defaultAgent` in `~/.acpx/config.json` from `codex` to `omp` when it is still `codex`. Commands of the same name installed any other way are left alone. +- `agents` profile, omp plugins: ponytail ([DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)), i-have-adhd ([ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd)) and caveman ([JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)) from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. These are the only installs that are not checksum-verified: no publisher posts a checksum for them, so they track each author's default branch and load as agent instructions and hooks. The operator accepted this to keep them at the latest commit. - `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most seven. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. diff --git a/docs/fleet-guards.md b/docs/fleet-guards.md index dd6bd31..467981a 100644 --- a/docs/fleet-guards.md +++ b/docs/fleet-guards.md @@ -29,7 +29,7 @@ Sizing per lane count and the full list of pruners are in | `oss-fleet/doctor/dev-server-reaper.sh` | Every 2 minutes: kills `next dev`/`next-server`/`tsc --noEmit` trees in treehouse worktrees whose lane last reported `done:`/`paused:`/`blocked:`/`failed:`, has no agent process, or whose agent transcript is idle >= 30 min (`REAPER_IDLE_MIN`). A dev server is 3-4 GB and restarts in 10 s; idle ones from finished lanes are what filled swap. One `next dev` per branch is inherent - Next compiles the whole app per process - so the fix is lifetime, not sharing. | | `oss-fleet/doctor/storage-guard.sh` | Every 5 minutes: use% of the filesystems holding `/`, `/var/log`, the home and Docker's data root. WARN (85%) alerts once per episode, CRIT (92%) prunes only regenerable Docker data, and a fill rate projecting the disk full within 6 hours alerts even below WARN. See [Storage guard](#storage-guard). | | `oss-fleet/doctor/devtools-bridge-reaper.sh` | Every 10 minutes: stops attached chrome-devtools-axi bridges (`CHROME_DEVTOOLS_AXI_BROWSER_URL` set) whose process tree used no CPU and whose session state files did not change for 60 min (`REAPER_IDLE_MIN`). See [Devtools-bridge reaper](#devtools-bridge-reaper). | -| worktree `.env.fleet` (seeded, git-excluded) | `NODE_OPTIONS=--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`). The managed shell and Herdr environment set `BUN_OPTIONS="--env-file=.env --env-file=.env.local --env-file=.env.fleet"` (an explicit `--env-file` replaces bun's default `.env` and `.env.local` autoload, so both are listed; the `NODE_ENV`-specific `.env.` files are not loaded), so every bun started in the worktree root (`bun run dev`, `bun run tsc`, and omp itself) passes the cap to the node processes it spawns, and a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. Bun started outside the worktree root finds no `.env.fleet` and runs uncapped. Hidden through the shared `.git/info/exclude`; never written when the repository tracks its own `.env.fleet`. | +| `.profile` managed block and `herdr.service` | `NODE_OPTIONS=--max-old-space-size=2048` (`factory_fleet_node_heap_mb`), exported only with `fleet_guards` on. Every node process the shell or Herdr starts (`bun run dev`, `bun run tsc`, and the dev servers they spawn) gets the cap, so a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. bun is given no `--env-file`, so it keeps loading its own `.env`, `.env.` and `.env.local` in every directory. | | `.local/bin/supabase` | Shim: `status`/`--version` pass through; every lifecycle or schema subcommand is refused with the reason. `npx supabase` bypasses it, which is why the Docker guard exists. | | `.config/systemd/user/flotilla-*.{service,timer,path}` | Login start + 5-minute keeper for the stack; the guard as a restart-always service; the seeder on pool changes, every 2 minutes and at login. | | `/etc/docker/daemon.json` | `init: true` and `live-restore: true` merged in (tasks/docker.yml, any profile with docker). | diff --git a/docs/security.md b/docs/security.md index c162245..fbaa7b0 100644 --- a/docs/security.md +++ b/docs/security.md @@ -37,6 +37,6 @@ This export does not rewrite the current host's firewall, SSH policy, account me ## Updates -Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (GitHub release-asset digests, Node's `SHASUMS256.txt`, rustup's `.sha256`, npm registry integrity, PyPI digests); a release without one is refused. That proves the download is the published artifact; it does not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. +Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (GitHub release-asset digests, Node's `SHASUMS256.txt`, rustup's `.sha256`, npm registry integrity, PyPI digests); a release without one is refused. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, they track each author's default branch, and they load as agent instructions and hooks. The operator accepted that to keep them at the latest commit. Checksums prove a download is the published artifact; they do not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. Back up project repositories and application data separately, using encrypted storage and an application-aware restore procedure. A successful environment bootstrap is not evidence that a database backup is recoverable. diff --git a/fleet/doctor/worktree-env-seed.sh b/fleet/doctor/worktree-env-seed.sh index 533910c..68bda82 100755 --- a/fleet/doctor/worktree-env-seed.sh +++ b/fleet/doctor/worktree-env-seed.sh @@ -32,41 +32,6 @@ seed_one() { install -m 600 "$SRC" "$f" && log "seeded $f" } -# Heap cap for everything bun runs in the worktree (bun run dev, tsc): the -# managed shell and Herdr environment set BUN_OPTIONS to load .env, .env.local -# and .env.fleet, so every bun started in the worktree root loads this file -# and hands NODE_OPTIONS to the node processes it spawns. A runaway compile -# then fails fast with a heap error the agent can see, instead of creeping to -# 4 GB and swapping the -# host. The file is hidden from git through the shared .git/info/exclude, so no -# lane ever sees or commits it. -HEAP_MB=${FLEET_NODE_HEAP_MB:-2048} -HEAP_FILE=.env.fleet -heap_content() { - printf '%s\n' \ - "$MARK (marker; fleet-managed, hidden via .git/info/exclude)" \ - "# Heap cap for bun-run scripts (next dev, tsc): fail fast instead of swapping the host." \ - "NODE_OPTIONS=--max-old-space-size=$HEAP_MB" -} -seed_heap_cap() { - local wt=$1 f=$1/$HEAP_FILE common tmp - if git -C "$wt" ls-files --error-unmatch "$HEAP_FILE" >/dev/null 2>&1; then - return 0 # the repository tracks its own file; never touch a tracked file - fi - common=$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null) || return 0 - case "$common" in /*) ;; *) common="$wt/$common" ;; esac - if [ -d "$common/info" ] && ! grep -qxF "$HEAP_FILE" "$common/info/exclude" 2>/dev/null; then - printf '%s\n' "$HEAP_FILE" >> "$common/info/exclude" && log "excluded $HEAP_FILE in $common/info/exclude" - fi - tmp=$(mktemp) && heap_content > "$tmp" - if [ -f "$f" ] && cmp -s "$tmp" "$f"; then rm -f "$tmp"; return 0; fi - if [ -f "$f" ] && ! grep -qF "$MARK" "$f"; then - cp -p "$f" "$f.pre-fleet-$(date -u +%Y%m%dT%H%M%SZ)" - fi - install -m 644 "$tmp" "$f" && log "seeded $f (heap cap ${HEAP_MB} MB)" - rm -f "$tmp" -} - # Worktree pools: ~/.treehouse/-//swarms-platform. A pool slot # directory can exist for a moment before `git worktree add` fills it; give the # checkout up to 2 minutes to appear so a brand-new lane is covered before its @@ -80,7 +45,6 @@ for slot in "$HOME"/.treehouse/swarms-platform-*/*/; do fi [ -e "$wt/package.json" ] || continue seed_one "$wt" - seed_heap_cap "$wt" done # Firstmate's primary checkout of the project. for wt in "$HOME"/.treehouse/firstmate-*/*/firstmate/projects/swarms-platform; do diff --git a/scripts/install_tools.py b/scripts/install_tools.py index e2f7b28..ae424ff 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -8,7 +8,9 @@ verified against the SHA-256 their publisher lists for that exact release (the GitHub release-asset digest, Node's SHASUMS256.txt, rustup's .sha256), npm tools against the integrity npm records for the resolved version, psutil against the -digests PyPI publishes. The Rust toolchain follows the stable channel. +digests PyPI publishes. The Rust toolchain follows the stable channel. The omp +marketplace plugins are the one exception: no publisher checksums them, so they +track each author's default branch. """ import argparse @@ -361,14 +363,18 @@ def npm_latest_install(home, tool, version, environment): return link_package_bins(home, final, [package_name]) or changed -def prune_dangling_links(home): - """Remove managed commands whose package is no longer installed.""" +def retire_legacy_npm(home): + """Remove the codex, pnpm and pnpx links into the dropped npm prefix, then that prefix.""" + legacy = home / ".local/share/code-factory/npm" changed = False - for link in (home / ".local/bin").iterdir(): - if link.is_symlink() and not link.exists(): - if Path(os.readlink(link)).is_relative_to(home / ".local/share/code-factory"): - link.unlink() - changed = True + for name in ("codex", "pnpm", "pnpx"): + link = home / ".local/bin" / name + if link.is_symlink() and Path(os.readlink(link)).is_relative_to(legacy): + link.unlink() + changed = True + if legacy.is_dir(): + shutil.rmtree(legacy) + changed = True return changed @@ -481,7 +487,9 @@ def main(): "obscura, supabase, psutil", ) parser.add_argument( - "--resolve", action="store_true", help="print the latest releases as JSON and exit" + "--resolve", + action="store_true", + help="print the latest release of every selected source as JSON and exit", ) parser.add_argument( "--resolved", type=json.loads, help="--resolve output to install instead of resolving" @@ -514,14 +522,16 @@ def main(): if args.npm: for tool in NPM_LATEST: changed = npm_latest_install(home, tool, latest[tool], environment) or changed - changed = prune_dangling_links(home) or changed + changed = retire_legacy_npm(home) or changed changed = omp_plugins(home, environment) or changed if args.development: changed = rust_install(home, environment) or changed if args.resolved: # The record checks compare against, so a later upstream release cannot # make an unchanged install look wrong. - (prefix / "resolved.json").write_text(json.dumps(latest, sort_keys=True) + "\n") + record = prefix / "resolved.json" + known = json.loads(record.read_text()) if record.is_file() else {} + record.write_text(json.dumps({**known, **latest}, sort_keys=True) + "\n") print( json.dumps( { diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 8d3b9be..4ee0920 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,6 +1,7 @@ import argparse import importlib.util import json +import os import shutil import subprocess import sys @@ -302,7 +303,7 @@ def test_verify_passes_when_firstmate_origin_is_the_configured_url(tmp_path): assert result.returncode == 0, result.stdout -def _ensure_status_row(tmp_path, config): +def _run_agents(tmp_path, start_at, **variables): playbook = tmp_path / "agents.yml" playbook.write_text( yaml.safe_dump( @@ -325,9 +326,9 @@ def _ensure_status_row(tmp_path, config): "localhost,", str(playbook), "--start-at-task", - "Read the omp config for the status row keys", + start_at, "--extra-vars", - json.dumps({"factory_omp_config": str(config), "ansible_become": False}), + json.dumps({"ansible_become": False, **variables}), ], cwd=tmp_path, capture_output=True, @@ -337,6 +338,21 @@ def _ensure_status_row(tmp_path, config): return int(result.stdout.rsplit("changed=", 1)[1].split()[0]) +def _ensure_status_row(tmp_path, config): + return _run_agents( + tmp_path, + "Read the omp config for the status row keys", + factory_omp_config=str(config), + factory_acpx_config=str(tmp_path / "absent-acpx.json"), + ) + + +def _ensure_acpx_default(tmp_path, config): + return _run_agents( + tmp_path, "Read the acpx config for the default agent", factory_acpx_config=str(config) + ) + + def test_existing_omp_config_gets_the_status_row_keys_once(tmp_path): config = tmp_path / "config.yml" config.write_text( @@ -395,3 +411,104 @@ def test_absent_omp_config_is_not_created_by_the_status_row_step(tmp_path): config = tmp_path / "config.yml" assert _ensure_status_row(tmp_path, config) == 0 assert not config.exists() + + +def test_acpx_config_that_still_defaults_to_codex_gets_omp_once(tmp_path): + config = tmp_path / "config.json" + seed = json.loads((ROOT / "config/acpx.json").read_text()) + config.write_text(json.dumps({**seed, "defaultAgent": "codex", "ttl": 900})) + config.chmod(0o600) + assert _ensure_acpx_default(tmp_path, config) == 1 + assert json.loads(config.read_text()) == {**seed, "ttl": 900} + assert config.stat().st_mode & 0o777 == 0o600 + written = config.read_text() + assert _ensure_acpx_default(tmp_path, config) == 0 + assert config.read_text() == written + + +@pytest.mark.parametrize("text", ['{"defaultAgent": "claude"}\n', '{"ttl": 300}\n']) +def test_acpx_config_with_another_default_is_left_alone(tmp_path, text): + config = tmp_path / "config.json" + config.write_text(text) + assert _ensure_acpx_default(tmp_path, config) == 0 + assert config.read_text() == text + + +def test_seeded_acpx_config_defaults_to_omp_and_absent_one_is_not_created(tmp_path): + config = tmp_path / "config.json" + assert _ensure_acpx_default(tmp_path, config) == 0 + assert not config.exists() + seed = (ROOT / "config/acpx.json").read_text() + config.write_text(seed) + assert _ensure_acpx_default(tmp_path, config) == 0 + assert config.read_text() == seed + + +def _managed_environment(tmp_path, fleet_guards): + variables = { + "factory_cfg": {"home": str(tmp_path), "profiles": {"fleet_guards": fleet_guards}}, + "factory_latest": {"chrome-devtools-mcp": "1.0.0"}, + } + result = subprocess.run( + [ + Path(sys.executable).parent / "ansible", + "localhost", + "-i", + "localhost,", + "-c", + "local", + "-m", + "ansible.builtin.debug", + "-a", + "var=factory_managed_shell_env", + "-e", + f"@{ROOT / 'ansible/group_vars/all.yml'}", + "-e", + json.dumps(variables), + ], + cwd=tmp_path, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + return json.loads(result.stdout.split("=>", 1)[1])["factory_managed_shell_env"] + + +def test_the_managed_environment_carries_the_heap_cap_only_with_fleet_guards(tmp_path): + assert set(_managed_environment(tmp_path, False)) == {"CHROME_DEVTOOLS_AXI_MCP_PATH"} + guarded = _managed_environment(tmp_path, True) + assert guarded["NODE_OPTIONS"] == "--max-old-space-size=2048" + assert "BUN_OPTIONS" not in guarded + + +@pytest.mark.skipif(not (shutil.which("bun") and shutil.which("node")), reason="needs bun and node") +def test_fleet_guards_cap_the_node_heap_and_leave_bun_env_loading_alone(tmp_path): + project = tmp_path / "project" + project.mkdir() + (project / ".env").write_text("FROM_DOTENV=1\n") + (project / ".env.test").write_text("FROM_TEST=1\n") + (project / "probe.test.ts").write_text( + 'import { expect, test } from "bun:test";\n' + 'test("env", () => {\n' + ' expect([process.env.FROM_DOTENV, process.env.FROM_TEST]).toEqual(["1", "1"]);\n' + "});\n" + ) + (project / "package.json").write_text( + json.dumps( + {"scripts": {"heap": "node -p \"require('v8').getHeapStatistics().heap_size_limit\""}} + ) + ) + inherited = { + key: value + for key, value in os.environ.items() + if key not in ("BUN_OPTIONS", "NODE_OPTIONS") + } + env = {**inherited, **_managed_environment(tmp_path, True)} + tests = subprocess.run(["bun", "test"], cwd=project, env=env, capture_output=True, text=True) + assert tests.returncode == 0, tests.stdout + tests.stderr + heap = subprocess.run( + ["bun", "run", "heap"], cwd=project, env=env, capture_output=True, text=True + ) + assert heap.returncode == 0, heap.stderr + limit = int(heap.stdout.strip().splitlines()[-1]) + assert 2048 * 2**20 <= limit < 2560 * 2**20 diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index aa98741..e49596c 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -239,26 +239,43 @@ def test_resolve_refuses_a_source_that_does_not_exist(monkeypatch, capsys, tmp_p run_cli(monkeypatch, capsys, tmp_path, "--tools", "uv", "--also", "nonesuch") -def test_commands_of_removed_packages_are_unlinked_but_user_links_kept(tmp_path): +def test_the_dropped_npm_set_is_retired_but_commands_installed_elsewhere_are_kept(tmp_path): bin_dir = tmp_path / ".local/bin" bin_dir.mkdir(parents=True) - managed = bin_dir / "codex" - managed.symlink_to(tmp_path / ".local/share/code-factory/npm/node_modules/gone/bin.js") - user = bin_dir / "mine" - user.symlink_to(tmp_path / "elsewhere/gone") - assert installer.prune_dangling_links(tmp_path) - assert not managed.is_symlink() - assert user.is_symlink() + legacy = tmp_path / ".local/share/code-factory/npm/node_modules" + for name in ("codex", "pnpm"): + script = legacy / name / "bin.js" + script.parent.mkdir(parents=True) + script.write_text("#!/bin/sh\n") + (bin_dir / name).symlink_to(script) + elsewhere = tmp_path / ".bun/bin/pnpx" + elsewhere.parent.mkdir(parents=True) + elsewhere.write_text("#!/bin/sh\n") + (bin_dir / "pnpx").symlink_to(elsewhere) + assert installer.retire_legacy_npm(tmp_path) + assert not (bin_dir / "codex").is_symlink() + assert not (bin_dir / "pnpm").is_symlink() + assert not (tmp_path / ".local/share/code-factory/npm").exists() + assert (bin_dir / "pnpx").resolve() == elsewhere + assert not installer.retire_legacy_npm(tmp_path) @pytest.mark.parametrize( ("argv", "expected"), - [([], {"herdr": {}}), (["--resolved", json.dumps({"uv": {}})], {"uv": {}})], + [ + ([], {"herdr": {"version": "1"}, "uv": {"version": "1"}}), + ( + ["--resolved", json.dumps({"uv": {"version": "2"}})], + {"herdr": {"version": "1"}, "uv": {"version": "2"}}, + ), + ], ) -def test_only_an_apply_with_a_resolved_record_rewrites_it(monkeypatch, tmp_path, argv, expected): +def test_an_apply_merges_its_resolved_record_into_the_existing_one( + monkeypatch, tmp_path, argv, expected +): record = tmp_path / ".local/share/code-factory/resolved.json" record.parent.mkdir(parents=True) - record.write_text(json.dumps({"herdr": {}}) + "\n") + record.write_text(json.dumps({"herdr": {"version": "1"}, "uv": {"version": "1"}}) + "\n") upstream(monkeypatch) monkeypatch.setattr(installer, "platform_key", lambda: "linux-x86_64") monkeypatch.setattr(installer, "install_asset", lambda *args: False) From a68b665549ee45e74761c72ff6e8b256293b84e5 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 02:40:04 +0000 Subject: [PATCH 08/13] no-mistakes(review): Recognize retired-prefix bridges; omp plugin exception docs; fix validate row --- .devcontainer/devcontainer.json | 3 ++- Dockerfile | 6 ++++-- README.md | 4 ++-- containers/factory.container.yml | 3 ++- docs/configuration.md | 2 +- maintenance/chrome-autoprune.py | 6 +++++- tests/test_chrome_autoprune.py | 36 ++++++++++++++++++++++++++++++++ 7 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/test_chrome_autoprune.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 021f3d1..c016805 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -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 latest, checksum-verified 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 diff --git a/Dockerfile b/Dockerfile index 01426c5..5557650 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,8 @@ # 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. Every tool the installer adds is -# its latest release, verified against the checksum its publisher posts. +# 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:latest @@ -151,7 +152,8 @@ RUN set -eux; ./factory validate --config "${FACTORY_CONFIG}" RUN set -eux; uv run --project . --locked python containers/assert-image-config.py "${FACTORY_CONFIG}" # The real convergence run. `apply` installs the latest, checksum-verified -# agent and development toolchain through scripts/install_tools.py and renders +# 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 diff --git a/README.md b/README.md index 934d14d..d74b8b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ⚡ 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 checksum-verified 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 @@ -8,7 +8,7 @@ flowchart TD boot --> init["./factory init: writes .local/host.yml"] init --> check["./factory validate, then plan"] check --> apply["./factory apply"] - lock["toolchain: latest releases, checksum-verified"] --> apply + lock["toolchain: latest releases, checksum-verified (omp plugins excepted)"] --> apply apply --> profiles["Ansible profiles"] subgraph host["Finished host"] herdr["Herdr workspace"] diff --git a/containers/factory.container.yml b/containers/factory.container.yml index fe78e09..2e94164 100644 --- a/containers/factory.container.yml +++ b/containers/factory.container.yml @@ -21,7 +21,8 @@ # process groups; it is native-only. # # profiles.agents and profiles.development stay true: the image installs the real -# latest, checksum-verified agent and development toolchain through scripts/install_tools.py. +# latest, checksum-verified agent and development toolchain through scripts/install_tools.py +# (the three omp marketplace plugins are the one unverified exception). schema_version: 1 factory: user: coder diff --git a/docs/configuration.md b/docs/configuration.md index f7a9de7..01e6936 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -7,7 +7,7 @@ | Command | What it does | | --- | --- | | `./factory init` | Copies `config/default.yml` to `.local/host.yml` with your user, home, and `~/Dev` workspace filled in. Never overwrites an existing file. Options: `--user`, `--home`, `--container`. | -| `./factory validate` | Checks the config against `schemas/factory.schema.json` and the cross-field rules below, | +| `./factory validate` | Checks the config against `schemas/factory.schema.json` and the cross-field rules below. | | `./factory plan` | Runs the Ansible playbook in check mode. Reports what would change; mutates nothing. | | `./factory apply` | Runs the playbook for real. Asks for the sudo password when passwordless sudo is not available. With the `firstmate` profile on, the first successful interactive apply with omp signed in then opens the new-host questions (below). | | `./factory doctor` | Checks that each expected tool runs and reports `gh` authentication. Changes nothing. | diff --git a/maintenance/chrome-autoprune.py b/maintenance/chrome-autoprune.py index ca8c30b..8cff12f 100755 --- a/maintenance/chrome-autoprune.py +++ b/maintenance/chrome-autoprune.py @@ -35,9 +35,11 @@ HOME = Path.home() REGISTRY = HOME / ".chrome-devtools-axi" # chrome-devtools-axi tracks its latest release, one prefix per version: -# //. Bridges from a superseded version stay prunable. +# //. Bridges from a superseded version stay prunable, +# as do bridges still running from the retired shared npm prefix. AXI_ROOT = HOME / ".local/share/code-factory/chrome-devtools-axi" BRIDGE = Path("node_modules/chrome-devtools-axi/dist/bin/chrome-devtools-axi-bridge.js") +LEGACY_BRIDGE = HOME / ".local/share/code-factory/npm" / BRIDGE DEFAULT_STATE = HOME / ".local/state/chrome-autoprune/state.json" UID = os.getuid() @@ -48,6 +50,8 @@ class Protected(Exception): def installed_bridge(path): script, root = Path(path).resolve(), AXI_ROOT.resolve() + if script == LEGACY_BRIDGE.resolve(): + return True return script.is_relative_to(root) and script.relative_to(root).parts[1:] == BRIDGE.parts diff --git a/tests/test_chrome_autoprune.py b/tests/test_chrome_autoprune.py new file mode 100644 index 0000000..9a0657a --- /dev/null +++ b/tests/test_chrome_autoprune.py @@ -0,0 +1,36 @@ +import importlib.util +import sys +import types +from pathlib import Path + +import pytest + +SCRIPT = Path(__file__).resolve().parents[1] / "maintenance/chrome-autoprune.py" +BRIDGE = "node_modules/chrome-devtools-axi/dist/bin/chrome-devtools-axi-bridge.js" + + +@pytest.fixture +def autoprune(tmp_path, monkeypatch): + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setitem(sys.modules, "psutil", types.ModuleType("psutil")) + spec = importlib.util.spec_from_file_location("chrome_autoprune", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.mark.parametrize( + ("relative", "expected"), + [ + (f".local/share/code-factory/chrome-devtools-axi/0.4.1/{BRIDGE}", True), + (f".local/share/code-factory/chrome-devtools-axi/0.3.0/{BRIDGE}", True), + (f".local/share/code-factory/npm/{BRIDGE}", True), + (f".local/share/code-factory/chrome-devtools-axi/{BRIDGE}", False), + (f".local/share/code-factory/other/{BRIDGE}", False), + (f".local/share/code-factory/npm/other/{BRIDGE}", False), + ], +) +def test_only_installed_or_retired_prefix_bridges_are_recognized( + autoprune, tmp_path, relative, expected +): + assert autoprune.installed_bridge(str(tmp_path / relative)) is expected From 839458fbb628d70ebabbc862c2716feed4003b62 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 03:11:41 +0000 Subject: [PATCH 09/13] no-mistakes(review): Accept legacy Obscura keys, drop restart, scope heap cap to lanes --- ansible/group_vars/all.yml | 14 ++---- ansible/tasks/fleet-browsers.yml | 1 - ansible/templates/herdr.service.j2 | 3 +- docs/capacity.md | 2 +- docs/configuration.md | 2 +- docs/fleet-guards.md | 7 ++- fleet/doctor/worktree-env-seed.sh | 24 +++++++++ schemas/factory.schema.json | 9 ++++ scripts/factory.py | 11 +++- tests/test_configuration.py | 65 ++++++++++-------------- tests/test_worktree_env_seed.py | 81 ++++++++++++++++++++++++++++++ 11 files changed, 163 insertions(+), 56 deletions(-) create mode 100644 tests/test_worktree_env_seed.py diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index a5e6acd..2f08831 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -56,15 +56,11 @@ factory_omp_root: "{{ factory_share_dir }}/omp/{{ factory_omp_version }}" factory_chrome_devtools_mcp_path: "{{ factory_share_dir }}/chrome-devtools-mcp/{{ factory_latest['chrome-devtools-mcp'] }}/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that -# provides the MCP entrypoint is installed by `--npm`. With fleet guards, -# NODE_OPTIONS caps the heap of every node process the shell or Herdr starts -# (bun run dev, tsc), so a runaway compile fails fast instead of swapping the -# host. bun keeps its own .env, .env. and .env.local autoload. -factory_fleet_node_heap_mb: 2048 -factory_managed_shell_env: >- - {{ {'CHROME_DEVTOOLS_AXI_MCP_PATH': factory_chrome_devtools_mcp_path} - | combine({'NODE_OPTIONS': '--max-old-space-size=' ~ factory_fleet_node_heap_mb} - if (factory_cfg.profiles.fleet_guards | bool) else {}) }} +# provides the MCP entrypoint is installed by `--npm`. Nothing here may be a +# process-wide runtime limit (NODE_OPTIONS, BUN_OPTIONS): it would also reach +# the chrome-devtools-axi bridge, chrome-devtools-mcp and acpx. +factory_managed_shell_env: + CHROME_DEVTOOLS_AXI_MCP_PATH: "{{ factory_chrome_devtools_mcp_path }}" # Contract: compute the unit ExecStart directly, never /usr/local/bin or $PATH. factory_herdr_bin: "{{ factory_tools_root }}/herdr/{{ factory_herdr_version }}/{{ factory_platform }}/herdr" factory_herdr_config_dir: "{{ factory_cfg.home }}/.config/herdr" diff --git a/ansible/tasks/fleet-browsers.yml b/ansible/tasks/fleet-browsers.yml index dee582f..40a1eef 100644 --- a/ansible/tasks/fleet-browsers.yml +++ b/ansible/tasks/fleet-browsers.yml @@ -88,7 +88,6 @@ group: "{{ factory_group }}" become: true loop: [obscura, obscura-worker] - notify: restart fleet browser obscura when: not ansible_check_mode - name: Inject the shared-browser env into shell profiles diff --git a/ansible/templates/herdr.service.j2 b/ansible/templates/herdr.service.j2 index 8a71b6a..92a2e54 100644 --- a/ansible/templates/herdr.service.j2 +++ b/ansible/templates/herdr.service.j2 @@ -14,8 +14,7 @@ ExecStart={{ factory_herdr_bin }} server WorkingDirectory={{ factory_cfg.workspace }} Environment=PATH=%h/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin {% if factory_cfg.profiles.agents | bool %} -# The installed MCP entrypoint, so AXI never resolves a floating npx package, and -# with fleet guards the node heap cap (NODE_OPTIONS). +# The installed MCP entrypoint, so AXI never resolves a floating npx package. {% for key, value in factory_managed_shell_env | dictsort %} Environment="{{ key }}={{ value }}" {% endfor %} diff --git a/docs/capacity.md b/docs/capacity.md index 4b50a31..855737b 100644 --- a/docs/capacity.md +++ b/docs/capacity.md @@ -12,7 +12,7 @@ Figures are estimates from a measured fleet host. | omp crewmate (one agent process) | 300-750 MB RSS | measured | | no-mistakes pipeline agent (`omp acp`) while a lane ships | about 450 MB | measured | | `next dev` / `next-server` for a UI lane | 3-4 GB | [fleet guards](fleet-guards.md) | -| `tsc --noEmit` | about 0.6 GB; node processes capped at a 2048 MB heap | measured; `NODE_OPTIONS` in the managed environment | +| `tsc --noEmit` | about 0.6 GB; lane node scripts capped at a 2048 MB heap | measured; the seeded worktree `node` wrapper ([fleet guards](fleet-guards.md)) | | chrome-devtools-axi bridge + `chrome-devtools-mcp` | about 0.3 GB for the MCP child; an idle bridge holds about 2 GB | measured | | Obscura browser tier | about 25 MB idle | [browser ladder](fleet-guards.md#browser-ladder) | | **Light lane** (docs, config, backend: agent + pipeline + tests) | **about 1.35-1.8 GB**; plan 2 GB | sum of the agent, pipeline and `tsc` rows | diff --git a/docs/configuration.md b/docs/configuration.md index 01e6936..271f994 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -56,7 +56,7 @@ factory: - `user` is not `root`, and `workspace` is inside `home`. - `firstmate` and `browser_prune.enabled` need `agents`. -- `fleet_guards` needs `docker` and `firstmate`. Obscura is always its latest release; remove any `browsers.obscura_version` or `browsers.obscura_sha256` left in an older `.local/host.yml`, which validation now rejects. +- `fleet_guards` needs `docker` and `firstmate`. Obscura is always its latest release. An older `.local/host.yml` that still sets `browsers.obscura_version` or `browsers.obscura_sha256` keeps working: both keys are deprecated, ignored, and reported in one warning on stderr. No edit is required. The Firstmate checkout tracks the default branch of upstream Firstmate, not a sha. Every apply fetches `origin/main` and fast-forwards `main`, so tracking it is how a host stays current. Do not re-pin it to a sha. Each run resolves `origin/main` once and reports the sha it installed. To track a fork, set `firstmate.url` in `.local/host.yml`. The URL applies to a fresh clone; verification fails when an existing checkout's `origin` is a different URL, and provisioning never changes it for you. To switch an existing checkout, run `git -C /firstmate remote set-url origin ` and `git -C /firstmate fetch origin`, reconcile any local commits on `main` with `origin/main` by hand, then run `./factory apply`. diff --git a/docs/fleet-guards.md b/docs/fleet-guards.md index 467981a..afb0e5b 100644 --- a/docs/fleet-guards.md +++ b/docs/fleet-guards.md @@ -25,11 +25,11 @@ Sizing per lane count and the full list of pruners are in | --- | --- | | `oss-fleet/shared-supabase/` | The ONE stack: the latest Supabase CLI (`npm install` of the registry's latest `supabase`), `supabase/config.toml` with `factory.fleet.supabase_project_id`, `check.sh` keeper, `guard.sql`, `README.md`. `check.sh` also generates the project's `.env.local` from the running stack. | | `oss-fleet/doctor/docker-guard.sh` | `docker events` watcher. A container carrying `com.supabase.cli.project` other than an allowlisted project is removed on creation; bare Postgres-family images are logged and alerted, not killed (other projects may own them). `docker-guard-allow.txt` is written once and then operator-owned. | -| `oss-fleet/doctor/worktree-env-seed.sh` | Installs the env file as `.env.local` in the project worktrees matched by the pool glob and Firstmate checkout path fixed in `fleet/doctor/worktree-env-seed.sh`; `factory.fleet.worktree_pools` only sets which pool directories the systemd path unit watches to trigger it. Files without the `# fleet-shared-supabase` marker are replaced with a backup left beside them. | +| `oss-fleet/doctor/worktree-env-seed.sh` | Installs the env file as `.env.local` in the project worktrees matched by the pool glob and Firstmate checkout path fixed in `fleet/doctor/worktree-env-seed.sh`; `factory.fleet.worktree_pools` only sets which pool directories the systemd path unit watches to trigger it. Files without the `# fleet-shared-supabase` marker are replaced with a backup left beside them. In each pool worktree whose `node_modules/.bin` exists it also seeds a marked `node_modules/.bin/node` wrapper that runs the managed `node` with `--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`) and is never written over a dependency's own `node` bin. See the heap cap row below. | | `oss-fleet/doctor/dev-server-reaper.sh` | Every 2 minutes: kills `next dev`/`next-server`/`tsc --noEmit` trees in treehouse worktrees whose lane last reported `done:`/`paused:`/`blocked:`/`failed:`, has no agent process, or whose agent transcript is idle >= 30 min (`REAPER_IDLE_MIN`). A dev server is 3-4 GB and restarts in 10 s; idle ones from finished lanes are what filled swap. One `next dev` per branch is inherent - Next compiles the whole app per process - so the fix is lifetime, not sharing. | | `oss-fleet/doctor/storage-guard.sh` | Every 5 minutes: use% of the filesystems holding `/`, `/var/log`, the home and Docker's data root. WARN (85%) alerts once per episode, CRIT (92%) prunes only regenerable Docker data, and a fill rate projecting the disk full within 6 hours alerts even below WARN. See [Storage guard](#storage-guard). | | `oss-fleet/doctor/devtools-bridge-reaper.sh` | Every 10 minutes: stops attached chrome-devtools-axi bridges (`CHROME_DEVTOOLS_AXI_BROWSER_URL` set) whose process tree used no CPU and whose session state files did not change for 60 min (`REAPER_IDLE_MIN`). See [Devtools-bridge reaper](#devtools-bridge-reaper). | -| `.profile` managed block and `herdr.service` | `NODE_OPTIONS=--max-old-space-size=2048` (`factory_fleet_node_heap_mb`), exported only with `fleet_guards` on. Every node process the shell or Herdr starts (`bun run dev`, `bun run tsc`, and the dev servers they spawn) gets the cap, so a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. bun is given no `--env-file`, so it keeps loading its own `.env`, `.env.` and `.env.local` in every directory. | +| Lane node heap cap | `bun run dev`, `bun run tsc` and the other scripts a lane runs through bun, npm or npx put the worktree's `node_modules/.bin` first on `PATH`, so the seeded `node` wrapper caps the dev server and type-check at a 2048 MB heap: a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. The cap is deliberately not in the `.profile` managed block, the Herdr unit or `BUN_OPTIONS`, which carry only `CHROME_DEVTOOLS_AXI_MCP_PATH`: a process-wide `NODE_OPTIONS` would also cap the chrome-devtools-axi bridge (about 2 GB idle), `chrome-devtools-mcp` and `acpx`, and `--env-file` would switch off bun's own `.env`, `.env.` and `.env.local` autoload. bun does not pass those files, `.npmrc` or `bunfig.toml` settings to a script's node child, which is why the wrapper is the seeded mechanism. `rm -rf node_modules` removes it until the next seeder run (at most 2 minutes). | | `.local/bin/supabase` | Shim: `status`/`--version` pass through; every lifecycle or schema subcommand is refused with the reason. `npx supabase` bypasses it, which is why the Docker guard exists. | | `.config/systemd/user/flotilla-*.{service,timer,path}` | Login start + 5-minute keeper for the stack; the guard as a restart-always service; the seeder on pool changes, every 2 minutes and at login. | | `/etc/docker/daemon.json` | `init: true` and `live-restore: true` merged in (tasks/docker.yml, any profile with docker). | @@ -122,6 +122,9 @@ Playwright Chromium). Obscura ships for x86_64 and aarch64; apply installs the latest release for the host's platform (see [Dependencies](dependencies.md#fleet-browsers-and-supabase)). +A new release never restarts a running tier: apply repoints the `obscura` +symlink, and the tier picks up the new binary at its next idle refresh or +restart, because Obscura restarts only when no lane is connected to it. All three tiers share one session. `cookie-sync.ts` keeps a canonical jar at `~/.fleet-browser/cookies.json` and converges every live tier to it over CDP. diff --git a/fleet/doctor/worktree-env-seed.sh b/fleet/doctor/worktree-env-seed.sh index 68bda82..4021d5e 100755 --- a/fleet/doctor/worktree-env-seed.sh +++ b/fleet/doctor/worktree-env-seed.sh @@ -32,6 +32,29 @@ seed_one() { install -m 600 "$SRC" "$f" && log "seeded $f" } +# Heap cap for what a lane runs through bun, npm or npx (next dev, tsc): those +# put the worktree's node_modules/.bin first on PATH, so a `node` there is the +# only node they start, and the cap reaches the dev server instead of the +# shell or Herdr environment (the chrome-devtools-axi bridge, chrome-devtools-mcp +# and acpx never see it). bun passes neither .env files nor NODE_OPTIONS set in +# them to a script's children. A runaway compile then fails fast with a heap +# error the agent can see, instead of growing to 4 GB and swapping the host. +HEAP_MB=${FLEET_NODE_HEAP_MB:-2048} +seed_node_cap() { + local dir=$1/node_modules/.bin real tmp + [ -d "$dir" ] || return 0 + real=$(command -v node) || return 0 + if [ -e "$dir/node" ] && ! grep -qF "$MARK" "$dir/node"; then + return 0 # a dependency ships its own node bin; never replace it + fi + tmp=$(mktemp) || return 0 + printf '#!/bin/sh\n%s (fleet-managed heap cap for scripts run in this worktree)\nexec %q --max-old-space-size=%s "$@"\n' \ + "$MARK" "$real" "$HEAP_MB" > "$tmp" + if [ -f "$dir/node" ] && cmp -s "$tmp" "$dir/node"; then rm -f "$tmp"; return 0; fi + install -m 755 "$tmp" "$dir/node" && log "seeded $dir/node (heap cap ${HEAP_MB} MB)" + rm -f "$tmp" +} + # Worktree pools: ~/.treehouse/-//swarms-platform. A pool slot # directory can exist for a moment before `git worktree add` fills it; give the # checkout up to 2 minutes to appear so a brand-new lane is covered before its @@ -45,6 +68,7 @@ for slot in "$HOME"/.treehouse/swarms-platform-*/*/; do fi [ -e "$wt/package.json" ] || continue seed_one "$wt" + seed_node_cap "$wt" done # Firstmate's primary checkout of the project. for wt in "$HOME"/.treehouse/firstmate-*/*/firstmate/projects/swarms-platform; do diff --git a/schemas/factory.schema.json b/schemas/factory.schema.json index b448c23..89732c2 100644 --- a/schemas/factory.schema.json +++ b/schemas/factory.schema.json @@ -257,6 +257,15 @@ "type": "object", "additionalProperties": false, "properties": { + "obscura_version": { + "type": "string", + "deprecated": true + }, + "obscura_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "deprecated": true + }, "idle_timeout_min": { "type": "integer", "minimum": 5 diff --git a/scripts/factory.py b/scripts/factory.py index 01b5f08..ddfdc89 100755 --- a/scripts/factory.py +++ b/scripts/factory.py @@ -15,6 +15,7 @@ ROOT = Path(__file__).resolve().parent.parent STALE_FIRSTMATE_URL = "https://github.com/undeemed/firstmate.git" +LEGACY_BROWSER_KEYS = ("obscura_version", "obscura_sha256") def validate_document(document, schema_name): @@ -67,7 +68,15 @@ def load_config(path): document = yaml.safe_load(path.read_text()) except yaml.YAMLError: raise ValueError("malformed host YAML; configuration contents omitted") from None - return validate_config(document) + validate_config(document) + if any(key in document["factory"].get("browsers", {}) for key in LEGACY_BROWSER_KEYS): + print( + "WARNING: factory.browsers.obscura_version and factory.browsers.obscura_sha256 " + "are no longer used and are ignored; Obscura always resolves to its latest release. " + f"Remove them from {path} when convenient.", + file=sys.stderr, + ) + return document def initialize(args): diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 4ee0920..d7ddc47 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,7 +1,6 @@ import argparse import importlib.util import json -import os import shutil import subprocess import sys @@ -25,6 +24,29 @@ def test_valid_configuration_is_accepted_by_real_schema(configuration): assert factory.validate_config(configuration) is configuration +def test_legacy_obscura_keys_are_ignored_with_one_warning_and_apply_proceeds( + configuration, tmp_path, monkeypatch, capsys +): + legacy = tmp_path / "legacy.yml" + current = tmp_path / "current.yml" + current.write_text(yaml.safe_dump(configuration)) + configuration["factory"]["browsers"].update(obscura_version="0.2.2", obscura_sha256="c" * 64) + legacy.write_text(yaml.safe_dump(configuration)) + provisioned = [] + monkeypatch.setattr( + factory, "provision", lambda document, check: provisioned.append(check) or 0 + ) + monkeypatch.setattr(factory, "questions", lambda document: 0) + for host in (current, legacy): + monkeypatch.setattr(factory.sys, "argv", ["factory", "apply", "--config", str(host)]) + assert factory.main() == 0 + warning = capsys.readouterr().err + assert provisioned == [False, False] + assert warning.count("WARNING") == 1 + assert "obscura_version" in warning and "obscura_sha256" in warning + assert "no longer used" in warning + + @pytest.mark.parametrize( "workspace", ["/home/other/Dev", "/home/coder/../other/Dev", "/home/coder"] ) @@ -474,41 +496,6 @@ def _managed_environment(tmp_path, fleet_guards): return json.loads(result.stdout.split("=>", 1)[1])["factory_managed_shell_env"] -def test_the_managed_environment_carries_the_heap_cap_only_with_fleet_guards(tmp_path): - assert set(_managed_environment(tmp_path, False)) == {"CHROME_DEVTOOLS_AXI_MCP_PATH"} - guarded = _managed_environment(tmp_path, True) - assert guarded["NODE_OPTIONS"] == "--max-old-space-size=2048" - assert "BUN_OPTIONS" not in guarded - - -@pytest.mark.skipif(not (shutil.which("bun") and shutil.which("node")), reason="needs bun and node") -def test_fleet_guards_cap_the_node_heap_and_leave_bun_env_loading_alone(tmp_path): - project = tmp_path / "project" - project.mkdir() - (project / ".env").write_text("FROM_DOTENV=1\n") - (project / ".env.test").write_text("FROM_TEST=1\n") - (project / "probe.test.ts").write_text( - 'import { expect, test } from "bun:test";\n' - 'test("env", () => {\n' - ' expect([process.env.FROM_DOTENV, process.env.FROM_TEST]).toEqual(["1", "1"]);\n' - "});\n" - ) - (project / "package.json").write_text( - json.dumps( - {"scripts": {"heap": "node -p \"require('v8').getHeapStatistics().heap_size_limit\""}} - ) - ) - inherited = { - key: value - for key, value in os.environ.items() - if key not in ("BUN_OPTIONS", "NODE_OPTIONS") - } - env = {**inherited, **_managed_environment(tmp_path, True)} - tests = subprocess.run(["bun", "test"], cwd=project, env=env, capture_output=True, text=True) - assert tests.returncode == 0, tests.stdout + tests.stderr - heap = subprocess.run( - ["bun", "run", "heap"], cwd=project, env=env, capture_output=True, text=True - ) - assert heap.returncode == 0, heap.stderr - limit = int(heap.stdout.strip().splitlines()[-1]) - assert 2048 * 2**20 <= limit < 2560 * 2**20 +@pytest.mark.parametrize("fleet_guards", [False, True]) +def test_the_managed_environment_never_carries_a_process_wide_runtime_limit(tmp_path, fleet_guards): + assert set(_managed_environment(tmp_path, fleet_guards)) == {"CHROME_DEVTOOLS_AXI_MCP_PATH"} diff --git a/tests/test_worktree_env_seed.py b/tests/test_worktree_env_seed.py new file mode 100644 index 0000000..d080209 --- /dev/null +++ b/tests/test_worktree_env_seed.py @@ -0,0 +1,81 @@ +"""fleet/doctor/worktree-env-seed.sh against a throwaway home and treehouse pool.""" + +import json +import os +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).parents[1] +SEED = ROOT / "fleet/doctor/worktree-env-seed.sh" +PROBE = "JSON.stringify([process.execArgv, require('v8').getHeapStatistics().heap_size_limit])" +CAP_FLAG = "--max-old-space-size=2048" +needs_node = pytest.mark.skipif(not shutil.which("node"), reason="needs node") + + +def seeded_worktree(home, bins=True): + (home / "oss-fleet/doctor").mkdir(parents=True) + shared = home / "oss-fleet/shared-supabase" + shared.mkdir() + (shared / "swarms-platform.env.local").write_text("# fleet-shared-supabase\nX=1\n") + worktree = home / ".treehouse/swarms-platform-abc123/1/swarms-platform" + worktree.mkdir(parents=True) + (worktree / "package.json").write_text(json.dumps({"scripts": {"probe": f'node -p "{PROBE}"'}})) + if bins: + (worktree / "node_modules/.bin").mkdir(parents=True) + return worktree + + +def seed(home): + env = { + key: value + for key, value in os.environ.items() + if key not in ("NODE_OPTIONS", "BUN_OPTIONS") + } + result = subprocess.run( + ["bash", SEED], env={**env, "HOME": str(home)}, capture_output=True, text=True + ) + assert result.returncode == 0, result.stderr + return env | {"HOME": str(home)} + + +def probed(command, worktree, env): + result = subprocess.run(command, cwd=worktree, env=env, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + return json.loads(result.stdout.strip().splitlines()[-1]) + + +@needs_node +@pytest.mark.skipif(not shutil.which("bun"), reason="needs bun") +def test_a_seeded_lane_script_gets_the_heap_cap_and_other_node_processes_do_not(tmp_path): + worktree = seeded_worktree(tmp_path) + env = seed(tmp_path) + flags, limit = probed(["bun", "run", "probe"], worktree, env) + assert CAP_FLAG in flags + assert 2048 * 2**20 <= limit < 2560 * 2**20 + flags, _ = probed(["node", "-p", PROBE], worktree, env) + assert CAP_FLAG not in flags + assert (worktree / ".env.local").read_text() == "# fleet-shared-supabase\nX=1\n" + + +@needs_node +def test_seeding_is_idempotent_and_never_replaces_a_dependencys_node_bin(tmp_path): + worktree = seeded_worktree(tmp_path) + seed(tmp_path) + wrapper = worktree / "node_modules/.bin/node" + first = wrapper.stat().st_mtime_ns + seed(tmp_path) + assert wrapper.stat().st_mtime_ns == first + wrapper.write_text("#!/bin/sh\necho dependency node\n") + seed(tmp_path) + assert wrapper.read_text() == "#!/bin/sh\necho dependency node\n" + + +@needs_node +def test_seeding_creates_no_node_modules_before_dependencies_are_installed(tmp_path): + worktree = seeded_worktree(tmp_path, bins=False) + seed(tmp_path) + assert not (worktree / "node_modules").exists() + assert (worktree / ".env.local").is_file() From b56436a2cd7b8c495beb3eb31a418152699d53b7 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 03:36:20 +0000 Subject: [PATCH 10/13] no-mistakes(review): Stable MCP link, keep legacy prefix, slot-level heap cap, smoke checks plugin record --- ansible/group_vars/all.yml | 11 ++++++----- docs/dependencies.md | 4 ++-- docs/fleet-guards.md | 4 ++-- fleet/doctor/worktree-env-seed.sh | 26 +++++++++++++------------- scripts/install_tools.py | 22 +++++++++++++++++----- tests/container-smoke.sh | 3 ++- tests/test_configuration.py | 12 ++++++++++-- tests/test_install_tools.py | 17 +++++++++++++++-- tests/test_worktree_env_seed.py | 28 +++++++++++++--------------- 9 files changed, 80 insertions(+), 47 deletions(-) diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 2f08831..968ab21 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -49,11 +49,12 @@ factory_local_bin: "{{ factory_cfg.home }}/.local/bin" factory_share_dir: "{{ factory_cfg.home }}/.local/share/code-factory" factory_tools_root: "{{ factory_share_dir }}/tools" factory_omp_root: "{{ factory_share_dir }}/omp/{{ factory_omp_version }}" -# AXI must launch the chrome-devtools MCP build this run installed (the latest -# release, under its versioned prefix) instead of resolving `npx ...@latest` -# at runtime. Exported to the managed shell environment and to the Herdr -# service environment. -factory_chrome_devtools_mcp_path: "{{ factory_share_dir }}/chrome-devtools-mcp/{{ factory_latest['chrome-devtools-mcp'] }}/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" +# AXI must launch the chrome-devtools MCP build the installer installed (the +# latest release) instead of resolving `npx ...@latest` at runtime. The +# installer re-points `current` at that release, so this text, and with it the +# Herdr unit and the profile block, stays the same between releases. Exported +# to the managed shell environment and to the Herdr service environment. +factory_chrome_devtools_mcp_path: "{{ factory_share_dir }}/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" # Environment shared by the interactive shell profile and the Herdr unit. # Consumed only when the agents profile is on, because the npm set that # provides the MCP entrypoint is installed by `--npm`. Nothing here may be a diff --git a/docs/dependencies.md b/docs/dependencies.md index ac22725..cbc000a 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -16,8 +16,8 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne - Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), uv ([astral-sh/uv](https://github.com/astral-sh/uv/releases/latest)), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. - Always: node, the newest release in the [nodejs.org index](https://nodejs.org/dist/index.json) (not the LTS line), verified against that release's `SHASUMS256.txt`. - `agents` profile, native: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. -- `agents` profile, npm: omp (`@oh-my-pi/pi-coding-agent`), chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, tasks-axi, acpx (runs the no-mistakes gate agent `acp:omp`), and chrome-devtools-mcp (the MCP build chrome-devtools-axi launches through `CHROME_DEVTOOLS_AXI_MCP_PATH`). The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. -- Retired: codex and pnpm are no longer installed (bun is the single package manager and runner; omp is the pi agent). An apply on a host that still has them removes the `codex`, `pnpm` and `pnpx` links in `~/.local/bin` that point into the old `~/.local/share/code-factory/npm` prefix, deletes that prefix, and changes `defaultAgent` in `~/.acpx/config.json` from `codex` to `omp` when it is still `codex`. Commands of the same name installed any other way are left alone. +- `agents` profile, npm: omp (`@oh-my-pi/pi-coding-agent`), chrome-devtools-axi, gh-axi, lavish-axi, quota-axi, tasks-axi, acpx (runs the no-mistakes gate agent `acp:omp`), and chrome-devtools-mcp (the MCP build chrome-devtools-axi launches through `CHROME_DEVTOOLS_AXI_MCP_PATH`, which points at `~/.local/share/code-factory/chrome-devtools-mcp/current`, a link the installer re-points at each release, so a new release never changes the Herdr unit or the `.profile` block and never restarts `herdr.service`). The fleet requires at least quota-axi 0.1.54 and tasks-axi 0.2.6. +- Retired: codex and pnpm are no longer installed (bun is the single package manager and runner; omp is the pi agent). An apply on a host that still has them removes the `codex`, `pnpm` and `pnpx` links in `~/.local/bin` that point into the old `~/.local/share/code-factory/npm` prefix and changes `defaultAgent` in `~/.acpx/config.json` from `codex` to `omp` when it is still `codex`. The old prefix stays on disk, like every superseded install, so shells and AXI bridges started before the upgrade keep their files; delete it by hand when nothing uses it. Commands of the same name installed any other way are left alone. - `agents` profile, omp plugins: ponytail ([DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)), i-have-adhd ([ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd)) and caveman ([JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)) from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. These are the only installs that are not checksum-verified: no publisher posts a checksum for them, so they track each author's default branch and load as agent instructions and hooks. The operator accepted this to keep them at the latest commit. - `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. diff --git a/docs/fleet-guards.md b/docs/fleet-guards.md index afb0e5b..e3edcc6 100644 --- a/docs/fleet-guards.md +++ b/docs/fleet-guards.md @@ -25,11 +25,11 @@ Sizing per lane count and the full list of pruners are in | --- | --- | | `oss-fleet/shared-supabase/` | The ONE stack: the latest Supabase CLI (`npm install` of the registry's latest `supabase`), `supabase/config.toml` with `factory.fleet.supabase_project_id`, `check.sh` keeper, `guard.sql`, `README.md`. `check.sh` also generates the project's `.env.local` from the running stack. | | `oss-fleet/doctor/docker-guard.sh` | `docker events` watcher. A container carrying `com.supabase.cli.project` other than an allowlisted project is removed on creation; bare Postgres-family images are logged and alerted, not killed (other projects may own them). `docker-guard-allow.txt` is written once and then operator-owned. | -| `oss-fleet/doctor/worktree-env-seed.sh` | Installs the env file as `.env.local` in the project worktrees matched by the pool glob and Firstmate checkout path fixed in `fleet/doctor/worktree-env-seed.sh`; `factory.fleet.worktree_pools` only sets which pool directories the systemd path unit watches to trigger it. Files without the `# fleet-shared-supabase` marker are replaced with a backup left beside them. In each pool worktree whose `node_modules/.bin` exists it also seeds a marked `node_modules/.bin/node` wrapper that runs the managed `node` with `--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`) and is never written over a dependency's own `node` bin. See the heap cap row below. | +| `oss-fleet/doctor/worktree-env-seed.sh` | Installs the env file as `.env.local` in the project worktrees matched by the pool glob and Firstmate checkout path fixed in `fleet/doctor/worktree-env-seed.sh`; `factory.fleet.worktree_pools` only sets which pool directories the systemd path unit watches to trigger it. Files without the `# fleet-shared-supabase` marker are replaced with a backup left beside them. For each pool worktree it also seeds a `node` wrapper in `/node_modules/.bin`, above the checkout, that runs the managed `node` with `--max-old-space-size=2048` (`FLEET_NODE_HEAP_MB`). See the heap cap row below. | | `oss-fleet/doctor/dev-server-reaper.sh` | Every 2 minutes: kills `next dev`/`next-server`/`tsc --noEmit` trees in treehouse worktrees whose lane last reported `done:`/`paused:`/`blocked:`/`failed:`, has no agent process, or whose agent transcript is idle >= 30 min (`REAPER_IDLE_MIN`). A dev server is 3-4 GB and restarts in 10 s; idle ones from finished lanes are what filled swap. One `next dev` per branch is inherent - Next compiles the whole app per process - so the fix is lifetime, not sharing. | | `oss-fleet/doctor/storage-guard.sh` | Every 5 minutes: use% of the filesystems holding `/`, `/var/log`, the home and Docker's data root. WARN (85%) alerts once per episode, CRIT (92%) prunes only regenerable Docker data, and a fill rate projecting the disk full within 6 hours alerts even below WARN. See [Storage guard](#storage-guard). | | `oss-fleet/doctor/devtools-bridge-reaper.sh` | Every 10 minutes: stops attached chrome-devtools-axi bridges (`CHROME_DEVTOOLS_AXI_BROWSER_URL` set) whose process tree used no CPU and whose session state files did not change for 60 min (`REAPER_IDLE_MIN`). See [Devtools-bridge reaper](#devtools-bridge-reaper). | -| Lane node heap cap | `bun run dev`, `bun run tsc` and the other scripts a lane runs through bun, npm or npx put the worktree's `node_modules/.bin` first on `PATH`, so the seeded `node` wrapper caps the dev server and type-check at a 2048 MB heap: a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. The cap is deliberately not in the `.profile` managed block, the Herdr unit or `BUN_OPTIONS`, which carry only `CHROME_DEVTOOLS_AXI_MCP_PATH`: a process-wide `NODE_OPTIONS` would also cap the chrome-devtools-axi bridge (about 2 GB idle), `chrome-devtools-mcp` and `acpx`, and `--env-file` would switch off bun's own `.env`, `.env.` and `.env.local` autoload. bun does not pass those files, `.npmrc` or `bunfig.toml` settings to a script's node child, which is why the wrapper is the seeded mechanism. `rm -rf node_modules` removes it until the next seeder run (at most 2 minutes). | +| Lane node heap cap | `bun run dev`, `bun run tsc` and the other scripts a lane runs through bun, npm or npx put every ancestor directory's `node_modules/.bin` on `PATH`, existing or not, so the seeded `node` wrapper in the pool slot directory above the checkout caps the dev server and type-check at a 2048 MB heap: a runaway `next dev`/`tsc` fails fast with a heap error the agent sees instead of swapping the host. The wrapper is written when the worktree appears, before the first `bun install`, so a fresh lane's first dev server is capped, and it lives outside the checkout, so `rm -rf node_modules` does not remove it. A `node` bin that a dependency puts in the worktree's own `node_modules/.bin` comes first on `PATH` and is left alone. The cap is deliberately not in the `.profile` managed block, the Herdr unit or `BUN_OPTIONS`, which carry only `CHROME_DEVTOOLS_AXI_MCP_PATH`: a process-wide `NODE_OPTIONS` would also cap the chrome-devtools-axi bridge (about 2 GB idle), `chrome-devtools-mcp` and `acpx`, and `--env-file` would switch off bun's own `.env`, `.env.` and `.env.local` autoload. bun does not pass those files, `.npmrc` or `bunfig.toml` settings to a script's node child, which is why the wrapper is the seeded mechanism. | | `.local/bin/supabase` | Shim: `status`/`--version` pass through; every lifecycle or schema subcommand is refused with the reason. `npx supabase` bypasses it, which is why the Docker guard exists. | | `.config/systemd/user/flotilla-*.{service,timer,path}` | Login start + 5-minute keeper for the stack; the guard as a restart-always service; the seeder on pool changes, every 2 minutes and at login. | | `/etc/docker/daemon.json` | `init: true` and `live-restore: true` merged in (tasks/docker.yml, any profile with docker). | diff --git a/fleet/doctor/worktree-env-seed.sh b/fleet/doctor/worktree-env-seed.sh index 4021d5e..6e7842a 100755 --- a/fleet/doctor/worktree-env-seed.sh +++ b/fleet/doctor/worktree-env-seed.sh @@ -33,23 +33,23 @@ seed_one() { } # Heap cap for what a lane runs through bun, npm or npx (next dev, tsc): those -# put the worktree's node_modules/.bin first on PATH, so a `node` there is the -# only node they start, and the cap reaches the dev server instead of the -# shell or Herdr environment (the chrome-devtools-axi bridge, chrome-devtools-mcp -# and acpx never see it). bun passes neither .env files nor NODE_OPTIONS set in -# them to a script's children. A runaway compile then fails fast with a heap -# error the agent can see, instead of growing to 4 GB and swapping the host. +# put every ancestor directory's node_modules/.bin on PATH, existing or not, so +# a `node` in the pool slot directory above the checkout is the only node they +# start, and the cap reaches the dev server instead of the shell or Herdr +# environment (the chrome-devtools-axi bridge, chrome-devtools-mcp and acpx +# never see it). It sits outside the checkout, so it exists before the first +# `bun install` and survives `rm -rf node_modules`. bun passes neither .env +# files nor NODE_OPTIONS set in them to a script's children. A runaway compile +# then fails fast with a heap error the agent can see, instead of growing to +# 4 GB and swapping the host. HEAP_MB=${FLEET_NODE_HEAP_MB:-2048} seed_node_cap() { local dir=$1/node_modules/.bin real tmp - [ -d "$dir" ] || return 0 real=$(command -v node) || return 0 - if [ -e "$dir/node" ] && ! grep -qF "$MARK" "$dir/node"; then - return 0 # a dependency ships its own node bin; never replace it - fi + mkdir -p "$dir" || return 0 tmp=$(mktemp) || return 0 - printf '#!/bin/sh\n%s (fleet-managed heap cap for scripts run in this worktree)\nexec %q --max-old-space-size=%s "$@"\n' \ - "$MARK" "$real" "$HEAP_MB" > "$tmp" + printf '#!/bin/sh\n# fleet-managed heap cap for the lane in this pool slot\nexec %q --max-old-space-size=%s "$@"\n' \ + "$real" "$HEAP_MB" > "$tmp" if [ -f "$dir/node" ] && cmp -s "$tmp" "$dir/node"; then rm -f "$tmp"; return 0; fi install -m 755 "$tmp" "$dir/node" && log "seeded $dir/node (heap cap ${HEAP_MB} MB)" rm -f "$tmp" @@ -68,7 +68,7 @@ for slot in "$HOME"/.treehouse/swarms-platform-*/*/; do fi [ -e "$wt/package.json" ] || continue seed_one "$wt" - seed_node_cap "$wt" + seed_node_cap "${slot%/}" done # Firstmate's primary checkout of the project. for wt in "$HOME"/.treehouse/firstmate-*/*/firstmate/projects/swarms-platform; do diff --git a/scripts/install_tools.py b/scripts/install_tools.py index ae424ff..492c387 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -360,11 +360,26 @@ def npm_latest_install(home, tool, version, environment): ) staging.rename(final) changed = True - return link_package_bins(home, final, [package_name]) or changed + changed = link_package_bins(home, final, [package_name]) or changed + if tool == "chrome-devtools-mcp": + changed = point_current(final) or changed + return changed + + +def point_current(target): + """Re-point `current` beside a version directory at it, so unit and profile text never change.""" + link = target.parent / "current" + if link.is_symlink() and link.resolve() == target.resolve(): + return False + staged = target.parent / ".current.new" + staged.unlink(missing_ok=True) + staged.symlink_to(target.name) + os.replace(staged, link) + return True def retire_legacy_npm(home): - """Remove the codex, pnpm and pnpx links into the dropped npm prefix, then that prefix.""" + """Remove the codex, pnpm and pnpx links into the dropped npm prefix; the prefix stays for live shells.""" legacy = home / ".local/share/code-factory/npm" changed = False for name in ("codex", "pnpm", "pnpx"): @@ -372,9 +387,6 @@ def retire_legacy_npm(home): if link.is_symlink() and Path(os.readlink(link)).is_relative_to(legacy): link.unlink() changed = True - if legacy.is_dir(): - shutil.rmtree(legacy) - changed = True return changed diff --git a/tests/container-smoke.sh b/tests/container-smoke.sh index d9a0f12..05adb6d 100755 --- a/tests/container-smoke.sh +++ b/tests/container-smoke.sh @@ -763,7 +763,8 @@ check_agent_gate() { printf '%s %s (floor %s)\n' "${name}" "${version}" "${floor}" done for name in ponytail i-have-adhd caveman; do - [ -d "${HOME}/.omp/plugins/cache/marketplaces/${name}" ] || fail "omp plugin ${name} is not installed" + jq -e --arg id "${name}@${name}" '.plugins[$id] | length > 0' "${HOME}/.omp/plugins/installed_plugins.json" >/dev/null \ + || fail "omp plugin ${name} is not installed" done printf 'gate agent acp:omp via acpx, ponytail-review on omp, omp plugins installed\n' } diff --git a/tests/test_configuration.py b/tests/test_configuration.py index d7ddc47..8e3f64b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -466,10 +466,10 @@ def test_seeded_acpx_config_defaults_to_omp_and_absent_one_is_not_created(tmp_pa assert config.read_text() == seed -def _managed_environment(tmp_path, fleet_guards): +def _managed_environment(tmp_path, fleet_guards, release="1.0.0"): variables = { "factory_cfg": {"home": str(tmp_path), "profiles": {"fleet_guards": fleet_guards}}, - "factory_latest": {"chrome-devtools-mcp": "1.0.0"}, + "factory_latest": {"chrome-devtools-mcp": release}, } result = subprocess.run( [ @@ -499,3 +499,11 @@ def _managed_environment(tmp_path, fleet_guards): @pytest.mark.parametrize("fleet_guards", [False, True]) def test_the_managed_environment_never_carries_a_process_wide_runtime_limit(tmp_path, fleet_guards): assert set(_managed_environment(tmp_path, fleet_guards)) == {"CHROME_DEVTOOLS_AXI_MCP_PATH"} + + +def test_a_new_chrome_devtools_mcp_release_leaves_the_managed_environment_unchanged(tmp_path): + before = _managed_environment(tmp_path, True, "1.0.0") + assert _managed_environment(tmp_path, True, "1.1.0") == before + assert before["CHROME_DEVTOOLS_AXI_MCP_PATH"].endswith( + "/chrome-devtools-mcp/current/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js" + ) diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index e49596c..f6137d3 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -239,7 +239,7 @@ def test_resolve_refuses_a_source_that_does_not_exist(monkeypatch, capsys, tmp_p run_cli(monkeypatch, capsys, tmp_path, "--tools", "uv", "--also", "nonesuch") -def test_the_dropped_npm_set_is_retired_but_commands_installed_elsewhere_are_kept(tmp_path): +def test_the_dropped_npm_set_loses_its_links_but_keeps_the_prefix_and_other_installs(tmp_path): bin_dir = tmp_path / ".local/bin" bin_dir.mkdir(parents=True) legacy = tmp_path / ".local/share/code-factory/npm/node_modules" @@ -255,11 +255,24 @@ def test_the_dropped_npm_set_is_retired_but_commands_installed_elsewhere_are_kep assert installer.retire_legacy_npm(tmp_path) assert not (bin_dir / "codex").is_symlink() assert not (bin_dir / "pnpm").is_symlink() - assert not (tmp_path / ".local/share/code-factory/npm").exists() + assert (legacy / "codex/bin.js").is_file() + assert (legacy / "pnpm/bin.js").is_file() assert (bin_dir / "pnpx").resolve() == elsewhere assert not installer.retire_legacy_npm(tmp_path) +def test_current_follows_the_newest_release_and_a_repeat_changes_nothing(tmp_path): + tool = tmp_path / "chrome-devtools-mcp" + for version in ("1.0.0", "1.1.0"): + (tool / version).mkdir(parents=True) + assert installer.point_current(tool / "1.0.0") + assert not installer.point_current(tool / "1.0.0") + assert (tool / "current").resolve() == tool / "1.0.0" + assert installer.point_current(tool / "1.1.0") + assert (tool / "current").resolve() == tool / "1.1.0" + assert (tool / "1.0.0").is_dir() + + @pytest.mark.parametrize( ("argv", "expected"), [ diff --git a/tests/test_worktree_env_seed.py b/tests/test_worktree_env_seed.py index d080209..a33c889 100644 --- a/tests/test_worktree_env_seed.py +++ b/tests/test_worktree_env_seed.py @@ -15,7 +15,7 @@ needs_node = pytest.mark.skipif(not shutil.which("node"), reason="needs node") -def seeded_worktree(home, bins=True): +def seeded_worktree(home): (home / "oss-fleet/doctor").mkdir(parents=True) shared = home / "oss-fleet/shared-supabase" shared.mkdir() @@ -23,8 +23,6 @@ def seeded_worktree(home, bins=True): worktree = home / ".treehouse/swarms-platform-abc123/1/swarms-platform" worktree.mkdir(parents=True) (worktree / "package.json").write_text(json.dumps({"scripts": {"probe": f'node -p "{PROBE}"'}})) - if bins: - (worktree / "node_modules/.bin").mkdir(parents=True) return worktree @@ -49,33 +47,33 @@ def probed(command, worktree, env): @needs_node @pytest.mark.skipif(not shutil.which("bun"), reason="needs bun") -def test_a_seeded_lane_script_gets_the_heap_cap_and_other_node_processes_do_not(tmp_path): +def test_a_fresh_lane_gets_the_heap_cap_before_and_after_its_first_install(tmp_path): worktree = seeded_worktree(tmp_path) env = seed(tmp_path) + assert not (worktree / "node_modules").exists() flags, limit = probed(["bun", "run", "probe"], worktree, env) assert CAP_FLAG in flags assert 2048 * 2**20 <= limit < 2560 * 2**20 + dev = worktree / "node_modules/.bin/dev" + dev.parent.mkdir(parents=True) + dev.write_text(f"#!/usr/bin/env node\nconsole.log({PROBE})\n") + dev.chmod(0o755) + (worktree / "package.json").write_text(json.dumps({"scripts": {"dev": "dev"}})) + flags, limit = probed(["bun", "run", "dev"], worktree, env) + assert CAP_FLAG in flags + assert 2048 * 2**20 <= limit < 2560 * 2**20 flags, _ = probed(["node", "-p", PROBE], worktree, env) assert CAP_FLAG not in flags assert (worktree / ".env.local").read_text() == "# fleet-shared-supabase\nX=1\n" @needs_node -def test_seeding_is_idempotent_and_never_replaces_a_dependencys_node_bin(tmp_path): +def test_seeding_is_idempotent_and_never_touches_the_checkouts_node_modules(tmp_path): worktree = seeded_worktree(tmp_path) seed(tmp_path) - wrapper = worktree / "node_modules/.bin/node" + wrapper = worktree.parent / "node_modules/.bin/node" first = wrapper.stat().st_mtime_ns seed(tmp_path) assert wrapper.stat().st_mtime_ns == first - wrapper.write_text("#!/bin/sh\necho dependency node\n") - seed(tmp_path) - assert wrapper.read_text() == "#!/bin/sh\necho dependency node\n" - - -@needs_node -def test_seeding_creates_no_node_modules_before_dependencies_are_installed(tmp_path): - worktree = seeded_worktree(tmp_path, bins=False) - seed(tmp_path) assert not (worktree / "node_modules").exists() assert (worktree / ".env.local").is_file() From dc2d3718d65669663c469659ecdb5945dbe7e53c Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 04:17:24 +0000 Subject: [PATCH 11/13] no-mistakes(test): Run omp plugin commands from the target home --- scripts/install_tools.py | 1 + tests/test_install_tools.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 492c387..46ecbe5 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -465,6 +465,7 @@ def omp(*argv): result = subprocess.run( [home / ".local/bin/omp", "plugin", *argv], env=environment, + cwd=home, capture_output=True, text=True, check=True, diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index f6137d3..28ce0d8 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -261,6 +261,25 @@ def test_the_dropped_npm_set_loses_its_links_but_keeps_the_prefix_and_other_inst assert not installer.retire_legacy_npm(tmp_path) +def test_omp_plugins_are_managed_from_the_target_home_whatever_the_installer_cwd( + monkeypatch, tmp_path +): + home = tmp_path / "home" + omp = home / ".local/bin/omp" + omp.parent.mkdir(parents=True) + omp.write_text( + "#!/bin/sh\n" + f'pwd >> "{tmp_path}/cwds"\n' + 'case "$2" in list) echo \'{"marketplace": []}\' ;; upgrade) echo "up to date" ;; esac\n' + ) + omp.chmod(0o755) + elsewhere = tmp_path / "checkout-under-another-users-home" + elsewhere.mkdir() + monkeypatch.chdir(elsewhere) + installer.omp_plugins(home, {}) + assert set((tmp_path / "cwds").read_text().split()) == {str(home)} + + def test_current_follows_the_newest_release_and_a_repeat_changes_nothing(tmp_path): tool = tmp_path / "chrome-devtools-mcp" for version in ("1.0.0", "1.1.0"): From 5fba19bfb5bde4c4edc617c313c42d332c853224 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 04:38:58 +0000 Subject: [PATCH 12/13] no-mistakes(document): Align docs and comments with latest-release toolchain --- CONTRIBUTING.md | 4 ++-- docs/architecture.md | 4 ++-- docs/omp.md | 2 +- docs/security.md | 2 +- maintenance/chrome-autoprune.py | 2 +- scripts/install_tools.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cda08d8..06abeb7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,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. -Nothing is pinned: every tool tracks its latest release, resolved once per apply and verified by the checksum its publisher posts, except the three omp marketplace plugins, which no publisher checksums and which track each author's default branch. Native assets are refused without a published SHA-256, npm tools install the exact resolved version, and the Rust toolchain follows `stable`. The installer records what it resolved in `~/.local/share/code-factory/resolved.json`. 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. +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. @@ -48,7 +48,7 @@ uv run ansible-playbook -i ansible/inventory.yml ansible/site.yml --syntax-check ## Adding a new tool 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. Add the tool name to `factory_core_tools` in `group_vars/all.yml` (or a profile-gated list). +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. diff --git a/docs/architecture.md b/docs/architecture.md index c089f41..d0ce39e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ Use Ansible core for the native Ubuntu host and Docker Compose for isolated workloads. Keep Herdr, SSH, the user service manager, and browser lifecycle management on the host. -The source machine already uses Ubuntu packages, user-level systemd services, home-directory tools, and SSH. Ansible manages those objects directly without moving the machine to a new package store or OS. The playbook consumes a validated host document; native tools have versioned URLs and SHA-256 values, npm has a dependency lock, and the provisioning Python environment has `uv.lock`. +The source machine already uses Ubuntu packages, user-level systemd services, home-directory tools, and SSH. Ansible manages those objects directly without moving the machine to a new package store or OS. The playbook consumes a validated host document; native tools are verified against publisher checksums, npm tools against registry integrity, and the provisioning Python environment has `uv.lock`. | Candidate | Fit here | Decision | | --- | --- | --- | @@ -53,7 +53,7 @@ Host sizing and every auto pruner are listed in [Capacity and pruners](capacity. ## Reproducibility policy -1. Everything latest, verified by published checksums. Each apply resolves the newest release of every tool the host installs, once (`scripts/install_tools.py --resolve`, with the same `--tools`, `--npm` and `--development` selection as the install), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release: the GitHub release-asset digest (herdr, bun, uv, gh, no-mistakes, treehouse, Obscura), Node's `SHASUMS256.txt` (the newest Node, not the LTS line), rustup's `.sha256` (rustup-init), the npm registry's integrity (omp, the AXI tools, acpx, chrome-devtools-mcp, the Supabase CLI), and PyPI's digests (psutil). A release without a published checksum is refused. The one exception is the omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, so each apply upgrades them in place to the latest commit of the author's default branch. The Rust toolchain follows `stable`, and the worker image builds `FROM ubuntu:latest`. +1. Everything latest, verified by published checksums. Each apply resolves the newest release of every tool the host installs, once (`scripts/install_tools.py --resolve`, with the same `--tools`, `--npm` and `--development` selection as the install), installs exactly that, and verifies each download against the checksum its publisher posts for that exact release. A release without a published checksum is refused; the one exception is the omp marketplace plugins, which no publisher checksums. What each source is verified against is in [Dependencies](dependencies.md) and [Primary sources](#primary-sources). 2. Do not copy a live global package directory. 3. Two locks stay, because they are this repository's own development environment rather than installed tools: change Python dependencies with `uv lock` and commit the lock, and keep each GitHub Action pinned to the commit SHA of its latest release, which `.github/dependabot.yml` advances weekly. 4. Keep machine differences in ignored `.local/host.yml`; schema validation precedes provisioning. diff --git a/docs/omp.md b/docs/omp.md index a6da662..45ea2d5 100644 --- a/docs/omp.md +++ b/docs/omp.md @@ -6,7 +6,7 @@ omp (`@oh-my-pi/pi-coding-agent`) is the agent harness Code Factory installs. Th With the `agents` profile on, `./factory apply`: -1. Installs the latest published omp, resolved from the npm registry on every apply (see [Dependencies](dependencies.md#latest-releases)), plus the omp plugins ponytail, i-have-adhd and caveman, upgraded to their latest release on every apply. +1. Installs the latest published omp, resolved from the npm registry on every apply, plus the omp plugins ponytail, i-have-adhd and caveman, upgraded on every apply (see [Dependencies](dependencies.md#latest-releases)). 2. Copies [`config/omp.yml`](../config/omp.yml) to `~/.omp/agent/config.yml` (directory `0700`, file `0600`), and [`config/omp-lsp.json`](../config/omp-lsp.json) to `~/.omp/agent/lsp.json`, which disables the markdown language server (marksman): it costs each session about 90 MB, and markdown diagnostics add nothing to agent work. 3. Installs the extension `~/.omp/agent/extensions/code-factory-herdr-sidebar.ts`, which feeds the Herdr Agent sidebar the session topic, the pane's short name, and the pull request line (pull request, issue and diff size). Every apply rewrites it. See [Herdr sidebar](herdr.md). 4. Installs the extension `~/.omp/agent/extensions/aa-mode-icons.ts` from [`config/omp-status-icons.ts`](../config/omp-status-icons.ts). Every apply rewrites it. See [Status line icons](#status-line-icons). diff --git a/docs/security.md b/docs/security.md index fbaa7b0..c0afbc5 100644 --- a/docs/security.md +++ b/docs/security.md @@ -37,6 +37,6 @@ This export does not rewrite the current host's firewall, SSH policy, account me ## Updates -Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (GitHub release-asset digests, Node's `SHASUMS256.txt`, rustup's `.sha256`, npm registry integrity, PyPI digests); a release without one is refused. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, they track each author's default branch, and they load as agent instructions and hooks. The operator accepted that to keep them at the latest commit. Checksums prove a download is the published artifact; they do not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. +Tools track their latest release, and every download is verified against the checksum its publisher posts for that release (what each source checks is in [Dependencies](dependencies.md)); a release without one is refused. The one exception is the three omp marketplace plugins (ponytail, i-have-adhd, caveman): no publisher checksums them, they track each author's default branch, and they load as agent instructions and hooks. The operator accepted that to keep them at the latest commit. Checksums prove a download is the published artifact; they do not establish that a publisher is trustworthy. Review added tools and installer behavior before adding them. Ubuntu security updates remain an operating-system responsibility rather than freezing an entire vulnerable package index forever. Back up project repositories and application data separately, using encrypted storage and an application-aware restore procedure. A successful environment bootstrap is not evidence that a database backup is recoverable. diff --git a/maintenance/chrome-autoprune.py b/maintenance/chrome-autoprune.py index 8cff12f..5b0c0de 100755 --- a/maintenance/chrome-autoprune.py +++ b/maintenance/chrome-autoprune.py @@ -11,7 +11,7 @@ excluded. No SIGKILL, profile deletion, package patching, or dev-server cleanup. The bridge drains HTTP requests and closes its own MCP/Chrome children on exit. -Requires Linux pidfds and the pinned private psutil runtime. Timer: chrome-autoprune.timer. +Requires Linux pidfds and the hash-verified private psutil runtime. Timer: chrome-autoprune.timer. Inspect with ~/.local/share/code-factory/pruner-venv/bin/python ~/.local/bin/chrome-autoprune.py Logs: journalctl --user -u chrome-autoprune.service Disable: systemctl --user disable --now chrome-autoprune.timer diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 46ecbe5..269fb0c 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -450,7 +450,7 @@ def rust_install(home, environment): return changed -# omp marketplace plugins, kept at their latest release: name -> GitHub repository. +# omp marketplace plugins, upgraded on every apply to the default branch: name -> repository. OMP_PLUGINS = { "ponytail": "DietrichGebert/ponytail", "i-have-adhd": "ayghri/i-have-adhd", @@ -459,7 +459,7 @@ def rust_install(home, environment): def omp_plugins(home, environment): - """Install each plugin from its marketplace, then upgrade all to the latest release.""" + """Install each plugin from its marketplace, then upgrade all to its default branch.""" def omp(*argv): result = subprocess.run( From 1b588184237f09a99bc5fbc16e68d5a1e77c2e92 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Thu, 1 Oct 2026 04:43:46 +0000 Subject: [PATCH 13/13] no-mistakes(document): Remove stale pinned wording and lock-era references --- ansible/tasks/fleet_guards.yml | 7 ++++--- ansible/tasks/tools.yml | 2 +- cloud-init/user-data.yaml | 8 ++++---- docs/configuration.md | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/ansible/tasks/fleet_guards.yml b/ansible/tasks/fleet_guards.yml index fa16480..72c3ed9 100644 --- a/ansible/tasks/fleet_guards.yml +++ b/ansible/tasks/fleet_guards.yml @@ -9,7 +9,8 @@ # any Supabase CLI project other than the shared one on creation and alerts # on bare Postgres containers. Allowlist is first-write (operator-owned). # * ~/oss-fleet/doctor/worktree-env-seed.sh - installs the stack's env file -# as .env.local in every swarms worktree; path unit + timer + login. +# as .env.local in every swarms worktree, plus a `node` wrapper in each +# pool slot that caps the lane heap; path unit + timer + login. # * ~/oss-fleet/doctor/dev-server-reaper.sh - every 2 min, kills next dev / # tsc trees in worktrees whose lane is done/paused/blocked, agent-less or # idle >= 30 min. Dev servers are throwaway; idle ones filled swap. @@ -31,8 +32,8 @@ # useless stack. # # Everything is idempotent and check-mode safe: file/copy/template preview; -# npm ci, volume restore and the first `supabase start` are skipped in check -# mode and reported instead. +# the Supabase CLI `npm install`, volume restore and the first +# `supabase start` are skipped in check mode and reported instead. - name: Ensure the fleet directories ansible.builtin.file: diff --git a/ansible/tasks/tools.yml b/ansible/tasks/tools.yml index a4bf115..6109895 100644 --- a/ansible/tasks/tools.yml +++ b/ansible/tasks/tools.yml @@ -97,7 +97,7 @@ changed_when: false when: not ansible_check_mode -- name: Assert the managed symlink resolves to the locked executable +- name: Assert the managed symlink resolves to the resolved executable ansible.builtin.assert: that: - factory_herdr_link.stat.exists | default(false) diff --git a/cloud-init/user-data.yaml b/cloud-init/user-data.yaml index 0dd7567..37b19e6 100644 --- a/cloud-init/user-data.yaml +++ b/cloud-init/user-data.yaml @@ -5,9 +5,9 @@ # # What this does # Installs the operating-system packages needed to *run* Code Factory: -# git to obtain the checkout, python3 + python3-venv for the pinned uv -# bootstrap, acl because Ansible escalates from one unprivileged account to -# another, and the archive/transport tools the pinned installer uses. +# git to obtain the checkout, python3 + python3-venv for the uv bootstrap, +# acl because Ansible escalates from one unprivileged account to another, +# and the archive/transport tools the tool installer uses. # # What this deliberately does not do # * no SSH keys, passwords, API tokens, auth keys or node identities - add @@ -53,7 +53,7 @@ packages: - unzip - xz-utils - zstd - # Used by the CLI, the pinned uv bootstrap and the playbook + # Used by the CLI, the uv bootstrap and the playbook - python3 - python3-venv - jq diff --git a/docs/configuration.md b/docs/configuration.md index 271f994..f22a062 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -76,11 +76,11 @@ The recipe refuses to overwrite a conflicting unmanaged command or an independen | Profile | Default | What it installs | | --- | --- | --- | | `agents` | on | omp, AXI tools, gh, no-mistakes, treehouse, acpx; safe omp presentation and model-role settings (see [omp configuration](omp.md)); first-write no-mistakes and acpx configs that make `acp:omp` the gate agent; `~/.local/bin/ponytail-review`; browser env defaults; Chrome autoprune timer. | -| `development` | on | Rust toolchain, build essentials, development-mode npm packages. | +| `development` | on | Rust toolchain (stable), build essentials. | | `firstmate` | on | Firstmate clone tracking upstream `main`, plus seeded Firstmate config: crew dispatch, crew and secondmate harness, the crew omp overlay (crew advisor, see [omp configuration](omp.md#advisor)), Herdr backend selection, startup memory budget, and the spawn memory floor. | | `docker` | on | Docker engine and Compose v2, with daemon defaults `init` (reaps orphaned children) and `live-restore`. Group membership is opt-in through the Ansible variable `factory_docker_group_users`. | | `fleet_guards` | off | Shared Supabase stack, Docker event guard, [browser ladder](fleet-guards.md#browser-ladder), dev-server reaper, devtools-bridge reaper, storage guard, env seeder. See [Fleet guards](fleet-guards.md). | | `tailscale` | off | Tailscale daemon only. Authentication is manual; see [Security](security.md#remote-access). | | `desktop` | off | Loopback-only XFCE + TigerVNC + noVNC operator desktop on `127.0.0.1:6080`, and the Google Chrome apt package. Needs an operator-created VNC password; see [Desktop access](recovery.md#desktop-access). Also supplies the TigerVNC/noVNC packages the browser ladder's `vnc` tier needs. | -The latest Herdr release is always installed, with the captured UI preferences, the [Spaces and Agents sidebar layouts](herdr.md) with the reporter timer that feeds Spaces, and one canonical, versioned user-service executable. Exact versions for every profile are in [Dependencies](dependencies.md). +The latest Herdr release is always installed, with the captured UI preferences, the [Spaces and Agents sidebar layouts](herdr.md) with the reporter timer that feeds Spaces, and one canonical, versioned user-service executable. What each profile installs, and where it comes from, is in [Dependencies](dependencies.md).