Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 36 additions & 30 deletions docs/LODY-MODELS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ model picker". The short answer: **model discovery is already dynamic and
nothing in BlitzOS or Lody needs a per-model edit — the only ceiling is the
`@anthropic-ai/claude-code` version the box is running.** §4 measures the whole
path end to end: a `claude update` on a live box put Fable 5.1 in the composer
with no rebake, no code change and no vendor bump. What is missing is only the
trigger — the automatic updater is off in four places.
with no rebake, no code change and no vendor bump. Vendor background checks do
not install updates in headless runs. The payload-owned `agent-cli-update`
service now supplies the missing trigger.

## 1. The discovery chain (dynamic, end to end)

Expand Down Expand Up @@ -51,8 +52,8 @@ either. `CLAUDE_MODEL_CONFIG` parses to the same two keys

## 3. What actually blocks Fable 5.1

`packages/box/Dockerfile:36` pins `@anthropic-ai/claude-code@2.1.228`.
That binary has no `claude-fable-5-1` string at all (it knows
The measured box ran `@anthropic-ai/claude-code@2.1.228`.
That binary had no `claude-fable-5-1` string at all (it knew
`claude-fable-5`, `claude-opus-5`, `claude-sonnet-5`, `claude-mythos-5`,
`claude-haiku-4-5`), so step 1 above never reports it.

Expand Down Expand Up @@ -111,8 +112,7 @@ vendored static list was never consulted.
Lody launches through `BLITZ_CLAUDE_EXECUTABLE`. The `~/.local/bin` shadow
copy the Dockerfile comment warns about is what the **native** installer
produces; the npm-global path does not take it. `NPM_CONFIG_PREFIX` being
owned by uid 1000 is what makes the in-place rewrite possible — and the
Dockerfile says that ownership exists so `claude` *can* auto-update.
owned by uid 1000 is what makes the explicit in-place update possible.
- **The browser half re-probes unconditionally.**
`runStartupAcpCapabilitiesRefresh` has no staleness check, no version compare
and no cache: it refreshes every config every time it is called.
Expand All @@ -123,7 +123,7 @@ vendored static list was never consulted.
per runtime mount, i.e. once per load of the Lody surface.** A member who
reloads the tab after an update gets the new list.

### What was blocking it (removed 2026-09-01)
### Background checks do not install headless

`DISABLE_AUTOUPDATER=1` had been set in four places.
Three box places were the image-wide `ENV` in
Expand All @@ -132,45 +132,51 @@ and `rootfs/etc/profile.d/blitz-npm.sh`.
These sites cover s6 daemons, login shells, and direct commands.
The now-retired broker set the fourth site in its spawn environment.

The flag gated the **background** update check only — the explicit `claude
update` subcommand ignored it, which is why the run above worked with the flag
live in the environment.
The flag gated the **background** update check only. Removing it restored the
vendor check, not unattended installation. The explicit `claude update`
subcommand ignored the flag, which is why the run above worked while the flag
was still present.

All four are gone. `codex`'s shim now passes
`-c check_for_update_on_startup=true`, and `@anthropic-ai/claude-code` is
installed `@latest` at build time rather than pinned. Nothing holds a CLI
version anymore.
Measured on 2026-09-05, Codex 0.147.0 wrote `latest_version`,
`last_checked_at`, and `dismissed_version`, but did not install an update.
Lody starts `codex app-server`, which never enters the TUI or accepts the
update keypress. Claude Code 2.1.228 also stayed at 2.1.228 across three
headless runs. Explicit commands with `NPM_CONFIG_PREFIX=/opt/blitz/npm`
updated Codex from 0.147.0 to 0.153.4 and Claude Code from 2.1.228 to 2.1.261.

## 5. Path forward

**Done 2026-09-01: the vendor's own auto-update path.** The flag is removed
from all four sites, codex's startup check is on, and the claude build pin is
`@latest`. An s6 oneshot driving `claude update` was considered and rejected as
redundant once the vendor updater is simply left alone.
**Done 2026-09-05: a payload-owned periodic updater.** The
`agent-cli-update` longrun waits briefly after boot and checks npm every five
minutes, matching the box payload poll. It runs an explicit `codex update` or
`claude update` only when that CLI's published version differs from its
installed version. It runs as blitz, uses the blitz-owned npm prefix, and logs
failures without stopping its loop. Unchanged states are logged at most hourly.
Both packages also use `@latest` when a fresh image is built.

The updater does not defer for active sessions. Measured in place, a running
`codex app-server` kept serving after its package changed. New sessions started
with the new binary.

The shadow-copy fear the old comments cited is handled independently:
`rootfs/etc/profile.d/blitz-npm.sh` force-moves `/usr/local/bin` to the FRONT of
PATH on every login shell, ahead of `/opt/blitz/npm/bin` (verified: a box login
shell gets `/usr/local/bin:/opt/blitz/npm/bin:…`). So a second copy in the npm
prefix cannot shadow the shim, and the native installer's `~/.local/bin` /
`~/.claude/local` are not on the box PATH at all. **Rewrite those comments when
the flag goes** — they are the justification the next agent will read, and they
will be wrong.
`~/.claude/local` are not on the box PATH at all. The Dockerfile, profile, and
shims now describe that PATH and update-service split directly.

Still outstanding:

1. **Land the image change on `main`.** The automatic canary `image` job
1. **Land the change on `main`.** The automatic canary `image` job
described in
[BOX-IMAGE.md](BOX-IMAGE.md#automatic-canary-image-publish) publishes and
pins the matching versioned R2 archive. There is no separate manual image
step; running boxes update their CLI in place, while fresh boxes start from
the newly pinned image.
pins the matching versioned R2 archive. The payload job delivers the updater
service to running boxes. Fresh boxes also start with current CLI packages.
2. **`@latest` costs this layer its reproducibility.** Two builds a week apart
ship different CLIs. That is the deliberate trade — the pin never held a
version in practice, because the first self-update moved it — but it means
the box image is no longer bit-reproducible from the Dockerfile alone.
`codex` stays pinned. Lody is independent: its daemon is identified by the
vendored upstream commit and build stamp.
can ship different CLIs. This means the box image is not bit-reproducible
from the Dockerfile alone. Lody is independent: its daemon is identified by
the vendored upstream commit and build stamp.

With no pin deciding which models exist, these four
`2.1.228` assertions need re-basing on a range or a probe rather than a
Expand Down
32 changes: 14 additions & 18 deletions packages/box/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,21 @@ RUN --mount=type=cache,id=lody-pnpm-node22-pnpm10.20,target=/root/.local/share/p

FROM node:22.20.0-bookworm-slim@sha256:b21fe589dfbe5cc39365d0544b9be3f1f33f55f3c86c87a76ff65a02f8f5848e AS vendors

# Agents install into a prefix the runtime user owns, so `claude` can
# auto-update and users can `npm install -g` without root.
# Agents install into a prefix the runtime user owns. The periodic updater and
# users can change global packages without root.
#
# `claude` is deliberately UNPINNED. Its version decides which models the Lody
# composer can offer (docs/LODY-MODELS.md), it updates itself on every box, and
# a build-time pin therefore only decided how stale a fresh box was on its first
# boot — it never held a version, because the first update moved it. Taking
# `@latest` at build time costs the reproducibility of this layer and buys a box
# that is current the moment it starts. `codex` stays pinned; Lody is installed
# into its independently switchable prefix in the next stage.
# Both CLIs are deliberately unpinned. The payload-owned agent-cli-update
# service runs their explicit update commands on existing boxes. Taking
# `@latest` here also makes a fresh box current when it starts.
# `vendor/lody-adapters/codex/package.json` declares
# `@openai/codex@^0.151.0`. The old 0.147.0 pin was below that contract. Lody
# uses its independent prefix in the next stage.
ENV NPM_CONFIG_PREFIX=/opt/blitz/npm
# `ws` carries no binary: it is the only dependency of
# /usr/local/libexec/blitz-lody-bridge, which reaches it through NODE_PATH.
RUN npm install --global --omit=dev \
@anthropic-ai/claude-code@latest \
@openai/codex@0.147.0 \
@openai/codex@latest \
ws@8.21.0 \
&& npm cache clean --force

Expand Down Expand Up @@ -198,14 +197,11 @@ RUN set -eux; \
# The Codex shim selects native login paths.
# The Claude shim starts the native CLI without changing authentication.
#
# THE VENDOR CLIs UPDATE THEMSELVES, and that is the point. A new Anthropic
# model reaches the Lody composer only when the `claude` binary is new enough to
# report it in its ACP `initialize` response — every layer above is passthrough
# (docs/LODY-MODELS.md). Holding the CLI at a build-time version therefore held
# the model list at build time too, and made a box-image rebake the delivery
# mechanism for models. It no longer is: `DISABLE_AUTOUPDATER` is gone from the
# image, the shims, and the profile. Codex's shim now leaves its
# startup update check on.
# THE PERIODIC SERVICE UPDATES BOTH VENDOR CLIs. A new Anthropic model reaches
# the Lody composer only when `claude` is new enough to report it in its ACP
# `initialize` response (docs/LODY-MODELS.md). The vendor background checks do
# not install an update in a headless process. The payload-owned
# agent-cli-update service runs both explicit update commands instead.
#
# What still protects the shims is the PATH order above, not a pin: an update
# rewrites /opt/blitz/npm in place, and /usr/local/bin sits ahead of it, so the
Expand Down
11 changes: 7 additions & 4 deletions packages/box/RECORD.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ reserved for boxes already in the field. Successor plan:

- One Linux OCI image. Platforms: `linux/amd64` + `linux/arm64`. Published
immutably. Run by digest. Mac runs the same Linux image. This replaces both
native installers (~1,275 duplicate lines), curl|sh, the host tarball,
NodeSource, and self-update.
native installers (~1,275 duplicate lines), curl|sh, the host tarball, and
NodeSource.
- Three externally reachable surfaces: key-only sshd · ttyd+tmux · the files
HTTP origin, which carries WebDAV, port discovery, and preview proxying. No
heartbeat. No exec jobs. No activity. No layout REST. No volume API.
- Claude and Codex run as their pinned official CLIs inside tmux. They read
- Claude and Codex run as official CLIs inside tmux. A payload-owned service
updates both in place. They read
the native HOME files on the state volume (`claude login` over ssh, once).
- The box-owned `blitz-cred api-token` helper refreshes machine credentials.
It carries no agent or control-plane API schema.
Expand All @@ -33,9 +34,11 @@ reserved for boxes already in the field. Successor plan:
- Supervision: pinned s6-overlay. Service graph: cgroups → init-state →
sshd · ttyd · dufs · HTTP gateway · Docker · agent services.
The credential refresher and rules sync also start after init-state.
The `agent-cli-update` longrun also starts after init-state.
The box runs alone with native HOME credentials.
The control plane remains an optional overlay.
- Image contents, all pinned by digest or version: `node:22-bookworm-slim` base
- Image contents use pinned digests or versions except for the two agent CLIs:
`node:22-bookworm-slim` base
(Node stays: the agent CLIs are Node; NodeSource dies),
openssh, tmux, git, ttyd (checksummed release), dufs 0.46.0 (checksummed
release), Claude Code, `@openai/codex`, static `blitz-cred`, and the
Expand Down
Loading
Loading