From 8c454ce8dcf732aad6e39807e3a78b3c5d065692 Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 06:17:26 +0000 Subject: [PATCH 1/3] feat(engine): ship the Rust vogt-core in the stack image (WI-1078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine image now carries the vogt-core it builds in server-build. The uv venv stage and the CORE_IMAGE stage that lifted the Python core are removed, along with the venv copy. vogt, vogt-core and vogt-mcp-remote are links to the one Rust binary; the entrypoint runs `vogt-core init` and `vogt-core serve --host … --port …` where it ran the Python CLI. The port and the health URL are unchanged. The Rust contract test now checks the engine image builds its own core rather than detecting a .rust-core marker, and a static test pins the Dockerfile and entrypoint shape. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- engine/Dockerfile | 86 +++++++------------------------- engine/deploy/entrypoint.sh | 6 +-- engine/server/tests/rust_core.rs | 40 +++++++++------ tests/test_engine_image_core.py | 58 +++++++++++++++++++++ 4 files changed, 105 insertions(+), 85 deletions(-) create mode 100644 tests/test_engine_image_core.py diff --git a/engine/Dockerfile b/engine/Dockerfile index 47d1493e8..3a5b01d34 100644 --- a/engine/Dockerfile +++ b/engine/Dockerfile @@ -24,9 +24,9 @@ # Stages: # 1. web-build — pnpm install + vite build of web/ # 1a. demo-web / demo-runtime — metadata-only augmentation + static demo image -# 2. server-build — cargo build --release of engine/server, embedding the bundle -# 3. core — the published public core image, lifted whole -# 4. runtime — `FROM ${POD_BASE_IMAGE}` plus both halves + entrypoint +# 2. server-build — cargo build --release of engine/server and vogt-core, +# embedding the bundle +# 3. runtime — `FROM ${POD_BASE_IMAGE}` plus both halves + entrypoint # # Stage order matters in one place: rust-embed reads `web/dist` at *compile* # time, so the web bundle is a build input to the Rust binary rather than a @@ -81,12 +81,8 @@ ARG NODE_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-base/node:22-bookworm # changed. The slim base carries 88 packages and neither of those. ARG DEMO_RUNTIME_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-base/node:22-bookworm-slim ARG RUST_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-base/rust:1-bookworm -# uv builds the core's virtualenv from the committed lockfile, so the image -# contains exactly what CI tested. Same pin as the root Dockerfile; -# they should move together. -ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.9.18 -# The dev-pod toolchain base — everything stage 4 used to install itself, and -# 22 of this image's 34 build minutes. Built and published by +# The dev-pod toolchain base — everything the shipped stage used to install +# itself, and 22 of this image's 34 build minutes. Built and published by # `.github/workflows/pod-base.yml` from `engine/Dockerfile.pod`; see either for # the variants and the tagging. # @@ -96,13 +92,6 @@ ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.9.18 # lives in the workflow rather than in this line, unlike the mirrored bases # above whose digests Renovate maintains here. ARG POD_BASE_IMAGE=ghcr.io/thedancingdeveloper-org/vogt-pod-base:lean -# The published public core image, lifted whole in stage 3. The default names -# the public `:latest` tag so `docker build -f engine/Dockerfile .` works from a -# clean clone with no `--build-arg`; it is a readable placeholder for a local -# build, not a supported way to produce a shipped image. CI always overrides it -# with a **digest** so a commit-identified build pins both halves — see stage 3 -# and `.github/workflows/build.yml`, exactly as `POD_BASE_IMAGE` above. -ARG CORE_IMAGE=ghcr.io/thedancingdeveloper-org/vogt:latest ARG VOGT_SOURCE_REF=local ARG VOGT_SOURCE_SHA=0000000000000000000000000000000000000000 ARG VOGT_PRODUCT_VERSION=local/dev @@ -254,35 +243,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ FROM scratch AS codex-gitguard-e2e COPY --from=codex-gitguard-e2e-build /codex_gitguard_e2e /codex_gitguard_e2e -FROM ${UV_IMAGE} AS uv - -# ─── Stage 3: the Python core, taken from the published image ─────────────── -# -# Not built here. This is the *published public core image*, digest-pinned by -# the caller, and `/opt/vogt` is lifted out of it whole. -# -# It used to be a second `uv sync` of the same source, which meant the merged -# image contained a core that was built like the public one rather than being -# it. "The private path is the public path plus configuration" was then a -# claim about two builds agreeing, checkable by nobody. Now it is a digest. -# -# This only works because the public image bundles its own interpreter under -# `/opt/vogt/python` (see the root Dockerfile). A venv is not self-contained -# by default: `bin/python` links to whatever created it, so a venv built on -# Debian and copied onto Ubuntu is a directory of scripts whose interpreter -# does not exist — and the error names the script, not the missing -# interpreter. Verified by copying `/opt/vogt` from the published image into -# a bare `ubuntu` and running `vogt --version`. -# -# CORE_IMAGE defaults to the public `:latest` tag (declared at the top of this -# file) so a bare `docker build` works from a clean clone. A floating tag would -# silently decouple the two halves of a commit-identified build, so `build.yml` -# always passes the digest its own core job just pushed — the default is the -# local-build convenience, never the shipped path. -ARG CORE_IMAGE -FROM ${CORE_IMAGE} AS core - -# ─── Stage 4: the shipped image ───────────────────────────────────────────── +# ─── Stage 3: the shipped image ───────────────────────────────────────────── # # Everything this stage used to install itself now lives in # `engine/Dockerfile.pod` and arrives as `POD_BASE_IMAGE`. It was 1344s @@ -603,30 +564,21 @@ COPY --from=server-build /usr/local/bin/vogt-engine /usr/local/bin/vogt-engine COPY --from=server-build /usr/local/bin/vogt-core /usr/local/bin/vogt-core RUN chmod +x /usr/local/bin/vogt-engine /usr/local/bin/vogt-core -# The other half. `/opt/vogt` carries the standalone interpreter *and* the -# venv built against it — see stage 3; splitting them here is how you get a -# `vogt` that reports its own script as missing. +# The other half is the Rust core, built in `server-build` and copied above, so +# nothing is lifted from another image. Three names on PATH, all one binary: # -# Symlinked onto PATH rather than prepending the venv's `bin` to it. That -# directory also holds `python` and `python3`, and a dev pod whose `python3` -# is silently the core's private interpreter is a pod where every agent's -# `pip install` goes somewhere surprising. Three names, no PATH surgery — and -# `vogt` on PATH is also what makes the core reachable from a session inside -# this container, which is the whole point of merging the trees. -# Two core layouts, one path. The Python image has a venv at -# `/opt/vogt/.venv/bin/vogt` (plus `vogt-mcp` and `vogt-mcp-remote`). The Rust -# image puts its static binary at the same path and a `.rust-core` marker -# beside it (`deploy/rust-core.Dockerfile`), so this COPY is identical for -# both and a Python build is unchanged. A Rust core has no Python MCP entry -# points; the engine's own `vogt-mcp` script, copied further down, is what a -# session runs. -COPY --from=core /opt/vogt /opt/vogt +# vogt — the operator's CLI (`docker exec vogt init`); it answers +# `init` and `serve` only, the verbs the Rust core has. +# vogt-core — the name the entrypoint runs the core under. +# vogt-mcp-remote — the stdio bridge. The binary chooses its mode from argv[0], +# so the name alone is what mcp-bootstrap and connect ask for. +# +# `vogt-mcp` is not linked: the engine's own script of that name, copied below, +# is what a session runs. RUN set -eu; \ - ln -sfn /opt/vogt/.venv/bin/vogt /usr/local/bin/vogt; \ - if [ ! -e /opt/vogt/.venv/bin/.rust-core ]; then \ - ln -sfn /opt/vogt/.venv/bin/vogt-mcp /usr/local/bin/vogt-mcp; \ - ln -sfn /opt/vogt/.venv/bin/vogt-mcp-remote /usr/local/bin/vogt-mcp-remote; \ - fi; \ + ln -sfn /usr/local/bin/vogt-core /usr/local/bin/vogt; \ + ln -sfn /usr/local/bin/vogt-core /usr/local/bin/vogt-mcp-remote; \ + vogt-core --version; \ vogt --version # The core's data directory (declared.sqlite3, observed.sqlite3, backups). diff --git a/engine/deploy/entrypoint.sh b/engine/deploy/entrypoint.sh index a1eb4faf8..23b852836 100755 --- a/engine/deploy/entrypoint.sh +++ b/engine/deploy/entrypoint.sh @@ -263,7 +263,7 @@ vogt_core_listen() { case "$host" in 127.0.0.1|localhost|'[::1]') - # `vogt serve --host` wants a bare address; the brackets are URL + # `vogt-core serve --host` wants a bare address; the brackets are URL # syntax, not part of the address. printf '%s %s\n' "${host//[\[\]]/}" "$port" ;; @@ -300,12 +300,12 @@ supervise_vogt_core() { # somebody restarts the container. while :; do started=$SECONDS - if ! vogt init; then + if ! vogt-core init; then echo "vogt-core: init failed — refusing to serve a store this" \ "build does not understand" >&2 else echo "vogt-core: serving on ${host}:${port}" - if vogt serve --host "$host" --port "$port"; then + if vogt-core serve --host "$host" --port "$port"; then echo "vogt-core: exited cleanly" >&2 else echo "vogt-core: exited with status $?" >&2 diff --git a/engine/server/tests/rust_core.rs b/engine/server/tests/rust_core.rs index cebda1d69..ca5d01165 100644 --- a/engine/server/tests/rust_core.rs +++ b/engine/server/tests/rust_core.rs @@ -430,28 +430,38 @@ async fn vogt_tools_lists_and_calls_the_real_rust_core() { } #[test] -fn the_engine_image_accepts_a_rust_core() { - // engine/Dockerfile branches on `/opt/vogt/.venv/bin/.rust-core`, the - // marker deploy/rust-core.Dockerfile writes. Without it the engine image - // would still demand a Python venv and CORE_IMAGE=vogt-core:rust would - // fail the build. +fn the_engine_image_builds_its_own_rust_core() { + // The engine image carries vogt-core from its own `server-build` stage. It + // no longer lifts a core from CORE_IMAGE or runs a uv venv, so neither a + // Python interpreter nor a second published image can reach the shipped + // image. The standalone Rust image (deploy/rust-core.Dockerfile) keeps its + // install path: the `vogt` service in the overlays still runs it. let dockerfile = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../Dockerfile"); let text = std::fs::read_to_string(&dockerfile).expect("engine Dockerfile"); assert!( - text.contains(".rust-core"), - "engine/Dockerfile no longer detects the Rust core layout" + text.contains("-p vogt-engine-server -p vogt-core"), + "engine/Dockerfile no longer builds vogt-core in server-build" ); + assert!( + text.contains("COPY --from=server-build /usr/local/bin/vogt-core /usr/local/bin/vogt-core"), + "engine/Dockerfile no longer copies vogt-core into the runtime image" + ); + for retired in [ + "CORE_IMAGE", + "UV_IMAGE", + "AS core", + "AS uv", + "/opt/vogt/.venv", + ] { + assert!( + !text.contains(retired), + "engine/Dockerfile still refers to the retired {retired}" + ); + } let rust = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../deploy/rust-core.Dockerfile"); let rust_text = std::fs::read_to_string(&rust).expect("rust core Dockerfile"); assert!( rust_text.contains("/opt/vogt/.venv/bin/vogt"), - "the Rust image no longer installs where the engine image copies from" - ); - let overlay = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../deploy/rust-core.overlay.yml"); - let overlay_text = std::fs::read_to_string(&overlay).expect("overlay"); - assert!( - overlay_text.contains("CORE_IMAGE"), - "the overlay must put the Rust core in the engine image, not only swap the vogt service" + "the Rust image no longer installs where the stack expects a core" ); } diff --git a/tests/test_engine_image_core.py b/tests/test_engine_image_core.py new file mode 100644 index 000000000..8ae7754cb --- /dev/null +++ b/tests/test_engine_image_core.py @@ -0,0 +1,58 @@ +"""The engine image runs the Rust core it builds itself (WI-1078, I3). + +Static checks on `engine/Dockerfile` and `engine/deploy/entrypoint.sh`. The +merged image no longer lifts a Python core from a published image or runs a uv +venv; `vogt-core` is built in `server-build` and the entrypoint starts it where +it used to start the Python `vogt`. The listen address and health URL are +unchanged, which is why the compose files do not change. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +ENGINE_DOCKERFILE = REPO_ROOT / "engine" / "Dockerfile" +ENTRYPOINT = REPO_ROOT / "engine" / "deploy" / "entrypoint.sh" + + +def _code_lines(path: Path) -> list[str]: + """Lines that are not whole-line comments, so prose cannot satisfy a check.""" + return [ + line + for line in path.read_text(encoding="utf-8").splitlines() + if not line.lstrip().startswith("#") + ] + + +def test_server_build_compiles_vogt_core_and_the_runtime_copies_it() -> None: + text = ENGINE_DOCKERFILE.read_text(encoding="utf-8") + assert "-p vogt-engine-server -p vogt-core" in text + assert ( + "COPY --from=server-build /usr/local/bin/vogt-core /usr/local/bin/vogt-core" + in text + ) + + +def test_the_engine_image_has_no_python_core_stage() -> None: + code = "\n".join(_code_lines(ENGINE_DOCKERFILE)) + for retired in ("UV_IMAGE", "CORE_IMAGE", "AS core", "AS uv", "/opt/vogt/.venv"): + assert retired not in code, f"engine/Dockerfile still refers to {retired}" + assert not re.search(r"COPY --from=core\b", code) + + +def test_vogt_names_resolve_to_the_rust_binary() -> None: + text = ENGINE_DOCKERFILE.read_text(encoding="utf-8") + assert "ln -sfn /usr/local/bin/vogt-core /usr/local/bin/vogt;" in text + assert "ln -sfn /usr/local/bin/vogt-core /usr/local/bin/vogt-mcp-remote;" in text + + +def test_the_entrypoint_runs_vogt_core_not_the_python_cli() -> None: + lines = _code_lines(ENTRYPOINT) + code = "\n".join(lines) + # A bare `vogt init` or `vogt serve` command would be the Python CLI; the + # `vogt-core` spelling has a hyphen, which the lookbehind rules out. + assert not any(re.search(r"(? Date: Sat, 17 Oct 2026 07:14:02 +0000 Subject: [PATCH 2/3] chore(build): drop CORE_IMAGE everywhere and fix stack-image docs (WI-1078) The stack image builds its own vogt-core, so nothing passes CORE_IMAGE any more. Remove it from the stack and workbench bake files, the merged-image jobs in build.yml (and their dependency on the core-image job, which only existed for the digest), the compose overlays, the smoke script and the rust-core and e2e workflows. The core-image job still publishes the root Dockerfile until I7; its comments no longer say the merged image lifts from it. Docs: DEPLOYMENT.md now says vogt-core init/serve and lists the Python CLI commands the stack image does not carry. CUSTOMISATION.md qualifies the virtualenv path as the Python core image's. ENGINE.md and engine/AGENTS.md no longer describe a CORE_IMAGE build. Co-Authored-By: Claude Haiku 5.5 Signed-off-by: sprooty --- .github/stack-bake.hcl | 6 ------ .github/workbench-bake.hcl | 5 ----- .github/workflows/build.yml | 29 ++++++++++------------------- .github/workflows/e2e.yml | 11 +++++------ .github/workflows/rust-core.yml | 2 +- deploy/engine.overlay.yml | 5 ----- deploy/rust-core.overlay.yml | 10 +++------- docs/CONTRIBUTING.md | 5 +++-- docs/CUSTOMISATION.md | 12 ++++++++---- docs/DEPLOYMENT.md | 22 +++++++++++++++++++--- docs/ENGINE.md | 17 +++++++---------- engine/AGENTS.md | 4 ++-- scripts/rust_core_compose_smoke.sh | 2 +- tests/test_public_delivery.py | 7 +++---- 14 files changed, 62 insertions(+), 75 deletions(-) diff --git a/.github/stack-bake.hcl b/.github/stack-bake.hcl index b9b0db8ec..bcb5816f7 100644 --- a/.github/stack-bake.hcl +++ b/.github/stack-bake.hcl @@ -22,10 +22,6 @@ variable "POD_BASE_IMAGE" { default = "" } -variable "CORE_IMAGE" { - default = "" -} - variable "VOGT_SOURCE_REF" { default = "" } @@ -43,7 +39,6 @@ target "stack" { dockerfile = "engine/Dockerfile" args = { POD_BASE_IMAGE = POD_BASE_IMAGE - CORE_IMAGE = CORE_IMAGE VOGT_SOURCE_REF = VOGT_SOURCE_REF VOGT_SOURCE_SHA = VOGT_SOURCE_SHA VOGT_PRODUCT_VERSION = VOGT_PRODUCT_VERSION @@ -69,7 +64,6 @@ target "gitguard" { target = "codex-gitguard-e2e" args = { POD_BASE_IMAGE = POD_BASE_IMAGE - CORE_IMAGE = CORE_IMAGE VOGT_SOURCE_REF = VOGT_SOURCE_REF VOGT_SOURCE_SHA = VOGT_SOURCE_SHA VOGT_PRODUCT_VERSION = VOGT_PRODUCT_VERSION diff --git a/.github/workbench-bake.hcl b/.github/workbench-bake.hcl index cb05c5e6a..7399b8069 100644 --- a/.github/workbench-bake.hcl +++ b/.github/workbench-bake.hcl @@ -18,10 +18,6 @@ variable "POD_BASE_IMAGE" { default = "" } -variable "CORE_IMAGE" { - default = "" -} - variable "VOGT_SOURCE_REF" { default = "" } @@ -39,7 +35,6 @@ target "stack" { dockerfile = "engine/Dockerfile" args = { POD_BASE_IMAGE = POD_BASE_IMAGE - CORE_IMAGE = CORE_IMAGE VOGT_SOURCE_REF = VOGT_SOURCE_REF VOGT_SOURCE_SHA = VOGT_SOURCE_SHA VOGT_PRODUCT_VERSION = VOGT_PRODUCT_VERSION diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fefb42117..5778ed9b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,9 +49,9 @@ permissions: contents: read env: - # The core alone. Built and pushed per commit because the merged image - # below copies `/opt/vogt` out of it by digest — a build input, not - # a deployment target (DEPLOYMENT.md). + # The core alone. Built and pushed per commit until I7 retires the root + # Dockerfile. The merged image no longer takes its core from here (WI-1078), + # so nothing downstream reads it; it is not a deployment target (DEPLOYMENT.md). IMAGE: ghcr.io/thedancingdeveloper-org/vogt # The product — the Python core *and* the session engine in one container, # which is what `deploy/stack.compose.yml` runs and the one supported @@ -133,10 +133,6 @@ jobs: contents: read packages: write id-token: write - # The merged image derives its core from this one by digest, so the - # digest has to leave this job rather than being re-derived from a tag. - outputs: - digest: ${{ steps.push.outputs.digest }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 @@ -271,9 +267,9 @@ jobs: { echo "### Built the core from \`${GITHUB_SHA:0:7}\`" echo - echo "A build input: the merged image job lifts its core from this" - echo "digest. This is not the image to pin — pin the *merged* stack" - echo "digest reported below." + echo "A standalone core image. The stack image no longer takes its" + echo "core from this one, so this is not the image to pin: pin the" + echo "*merged* stack digest reported below." echo echo '```' echo "${IMAGE}@${DIGEST}" @@ -615,10 +611,9 @@ jobs: stack-image: name: build and publish the merged image - # `needs: image` is not ordering for its own sake: the merged image copies - # /opt/vogt out of the core image this run just published, by digest. - # `needs: pod-base` supplies the toolchain base it now builds `FROM`. - needs: [base-images, pod-base, image] + # `needs: pod-base` supplies the toolchain base it now builds `FROM`. The + # core-image job is not a dependency: the stack builds its own vogt-core. + needs: [base-images, pod-base] runs-on: [self-hosted, publish] timeout-minutes: 60 permissions: @@ -691,7 +686,6 @@ jobs: id: push env: POD_BASE_IMAGE: ${{ needs.pod-base.outputs.image }} - CORE_IMAGE: ${{ env.IMAGE }}@${{ needs.image.outputs.digest }} VOGT_SOURCE_REF: ${{ github.ref_name }} VOGT_SOURCE_SHA: ${{ github.sha }} # metadata-action's `labels` output is newline-separated key=value @@ -712,7 +706,6 @@ jobs: # buildx refuses a local write without an explicit entitlement. --allow "fs.write=${E2E_DIR}" --set "*.args.POD_BASE_IMAGE=${POD_BASE_IMAGE}" - --set "*.args.CORE_IMAGE=${CORE_IMAGE}" --set "*.args.VOGT_SOURCE_REF=${VOGT_SOURCE_REF}" --set "*.args.VOGT_SOURCE_SHA=${VOGT_SOURCE_SHA}" --set "*.args.VOGT_PRODUCT_VERSION=0.7.17" @@ -839,7 +832,7 @@ jobs: stack-workbench-image: name: build and publish the workbench stack image - needs: [base-images, pod-base-workbench, image] + needs: [base-images, pod-base-workbench] runs-on: [self-hosted, publish] timeout-minutes: 60 permissions: @@ -884,7 +877,6 @@ jobs: id: push env: POD_BASE_IMAGE: ${{ needs.pod-base-workbench.outputs.image }} - CORE_IMAGE: ${{ env.IMAGE }}@${{ needs.image.outputs.digest }} VOGT_SOURCE_REF: ${{ github.ref_name }} VOGT_SOURCE_SHA: ${{ github.sha }} LABELS: ${{ steps.meta.outputs.labels }} @@ -896,7 +888,6 @@ jobs: --file .github/workbench-bake.hcl --metadata-file "$META_FILE" --set "*.args.POD_BASE_IMAGE=${POD_BASE_IMAGE}" - --set "*.args.CORE_IMAGE=${CORE_IMAGE}" --set "*.args.VOGT_SOURCE_REF=${VOGT_SOURCE_REF}" --set "*.args.VOGT_SOURCE_SHA=${VOGT_SOURCE_SHA}" --set "*.args.VOGT_PRODUCT_VERSION=0.7.17" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 652059bd8..8fe5bf7d2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -74,10 +74,9 @@ jobs: ENGINE_BIND: "127.0.0.1" ENGINE_PORT: "8910" ENGINE_PUBLIC_URL: "http://127.0.0.1:8910" - # Build the core image locally and point BOTH the core service's `image:` - # and the engine build's `CORE_IMAGE` arg (both read `${VOGT_IMAGE}`) at - # it, so nothing pulls the private GHCR core — the job runs with no - # registry credentials. + # Build the core image locally and point the core service's `image:` + # (it reads `${VOGT_IMAGE}`) at it, so nothing pulls the private GHCR + # core — the job runs with no registry credentials. VOGT_IMAGE: "vogt-core:local" steps: # A clean clone, every run: the whole point is that nothing on the runner @@ -113,11 +112,11 @@ jobs: set -euo pipefail # The core is private on GHCR and the runner has no registry # creds, so build everything from the repo's own Dockerfiles. VOGT_IMAGE - # points the core service and the engine build-arg at the local core. + # points the core service at the local core. # The e2e engine bakes in the fake-agent + preset, so nothing is # host-bind-mounted (the runner's FS is not the docker daemon's). docker build -t vogt-core:local -f Dockerfile . - docker build -t vogt-engine:local -f engine/Dockerfile --build-arg CORE_IMAGE=vogt-core:local . + docker build -t vogt-engine:local -f engine/Dockerfile . docker build -t vogt-engine-e2e:local -f deploy/e2e.engine.Dockerfile . # The live-Playwright image: web/ + browsers baked in so the # `live` project runs in-network with no bind-mount. Public base image, diff --git a/.github/workflows/rust-core.yml b/.github/workflows/rust-core.yml index 4435a8e85..04020d5f5 100644 --- a/.github/workflows/rust-core.yml +++ b/.github/workflows/rust-core.yml @@ -115,7 +115,7 @@ jobs: run: | set -euo pipefail docker build -t vogt-core:rust -f deploy/rust-core.Dockerfile . - docker build -t vogt-engine:local -f engine/Dockerfile --build-arg CORE_IMAGE=vogt-core:rust . + docker build -t vogt-engine:local -f engine/Dockerfile . docker build -t vogt-engine-e2e:local -f deploy/e2e.engine.Dockerfile . docker build -t vogt-e2e-playwright:local -f deploy/e2e.playwright.Dockerfile . # Tokens go through the named volume (VOGT_CORE_TOKEN_VALUE, diff --git a/deploy/engine.overlay.yml b/deploy/engine.overlay.yml index 73ba338c4..6a91f4994 100644 --- a/deploy/engine.overlay.yml +++ b/deploy/engine.overlay.yml @@ -57,11 +57,6 @@ services: context: .. dockerfile: engine/Dockerfile args: - # The published core, lifted whole into the engine image so one - # container carries both halves. Defaults to the same public image the - # base would run, so the embedded core matches it; pin a digest for a - # real deployment. The Dockerfile already carries a public default too. - CORE_IMAGE: "${VOGT_IMAGE:-ghcr.io/thedancingdeveloper-org/vogt:0.7.17}" # Optional image-managed agent CLIs. When enabled, versions come from # engine/agent-versions.env unless an operator deliberately overrides # one with a reproducible build arg. diff --git a/deploy/rust-core.overlay.yml b/deploy/rust-core.overlay.yml index 2392e8f60..133bb4d0e 100644 --- a/deploy/rust-core.overlay.yml +++ b/deploy/rust-core.overlay.yml @@ -4,10 +4,9 @@ # docker compose -f deploy/vogt.compose.yml -f deploy/engine.overlay.yml \ # -f deploy/rust-core.overlay.yml up -d # -# The `vogt` service is the core the engine fronts. The engine image also -# carries its own core (CORE_IMAGE), and a stack whose two halves are -# different binaries is not the thing under test. The build arg makes the -# engine image lift the same Rust image. +# The `vogt` service is the core the engine fronts. The engine image builds +# its own vogt-core from the same source, so no build arg is needed here; the +# core it fronts is still the `vogt` service above. # # VOGT_ENGINE_URL is what makes session.* on the core a call to the real # engine rather than an empty list with "no session engine is configured". @@ -64,9 +63,6 @@ services: - smoke-work:/home/sprooty/Working engine: - build: - args: - CORE_IMAGE: ${VOGT_IMAGE:-vogt-core:rust} depends_on: token-init: condition: service_completed_successfully diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b976921b5..576df9433 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -173,8 +173,9 @@ VOGT_IMAGE=vogt-core:local docker compose \ -f deploy/vogt.compose.yml -f deploy/engine.overlay.yml up --build -d ``` -`VOGT_IMAGE` names both the core the base runs and the `CORE_IMAGE` the engine -build lifts into its own image, so one variable points both at your build. The +`VOGT_IMAGE` names the core the base runs. The engine image builds its own +`vogt-core` from the same source rather than lifting one, so this variable is +the only thing to point at your build. The engine is then the front door at `http://127.0.0.1:8910`: the PWA at `/`, `/api/vogt/*` proxied to the core, `/mcp` for agents, `ENGINE_TOKEN` as the bearer. Both containers read the core token from `deploy/vogt-core-token`, so diff --git a/docs/CUSTOMISATION.md b/docs/CUSTOMISATION.md index 250618b18..5635e6701 100644 --- a/docs/CUSTOMISATION.md +++ b/docs/CUSTOMISATION.md @@ -365,15 +365,19 @@ whatever its image declares — frequently an empty anonymous volume that Docker creates fresh on each deploy. So **administrative commands go to the core**: `project import`, -`project register`, `token issue`, `status`. Anything you would run to +`project register`, `token issue`, `status`. These are Python-CLI verbs. The +core container here is the Python core image; the stack image has no Python +`vogt` and does not carry them (see [DEPLOYMENT.md](DEPLOYMENT.md), "The stack +image's CLI"). Anything you would run to change what the instance knows belongs where the instance's state is. The tempting `vogt init` that the error message suggests is exactly the wrong response — it would initialise a second, empty instance in the container that should not have one. -One wrinkle if you go looking: the core's CLI may be inside a virtualenv -(`/opt/vogt/.venv/bin`) that is on `PATH` for a plain `docker exec` but not -for a *login* shell. `docker exec core vogt status` finds it; +One wrinkle if you go looking, in the Python core image: its CLI is inside a +virtualenv (`/opt/vogt/.venv/bin`) that is on `PATH` for a plain `docker exec` +but not for a *login* shell. The stack image has no virtualenv; its `vogt` is +the Rust binary, linked into `/usr/local/bin`. `docker exec core vogt status` finds it; `docker exec core sh -lc 'vogt status'` may not, and reports `vogt: not found` — a missing binary that is not missing. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index b7ef9d290..f5a120611 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -29,7 +29,7 @@ sidecar, released as a pair and run by one Compose file, ├── /mcp proxied to the core ├── /api/auth/login, /api/install/... proxied untouched; the core self-gates └── /healthz, /readyz - vogt serve (:8000, loopback inside the container, never published) + vogt-core serve (:8000, loopback inside the container, never published) ├── /api/... REST (OpenAPI at /openapi.json) ├── /mcp MCP streamable HTTP transport └── /health/live, /health/ready, /version @@ -526,13 +526,29 @@ instance id), otherwise every difference is a conflict. fresh prod backup: the clone baseline is now behind both sides, so a second carry would report every item touched by the first as a conflict. -**Migrations run at boot.** The entrypoint runs `vogt init` before every -`vogt serve`. `init` is idempotent: it creates the instance on a new volume, +**Migrations run at boot.** The entrypoint runs `vogt-core init` before every +`vogt-core serve`. `init` is idempotent: it creates the instance on a new volume, brings an existing one forward to this build's schema, and leaves the audit history alone after that. Until the schemas match, `/health/ready` answers 503 naming the store and both schema numbers. Migrations are forward-only and run under a lock. Read [`SCHEMA.md`](SCHEMA.md) before a major version. +**The stack image's CLI is the Rust core.** On the stack image's PATH, `vogt`, +`vogt-core` and `vogt-mcp-remote` are one binary, `vogt-core`, built from +`engine/core` in the same build (WI-1078). It answers `init` (with `--check`) +and `serve`, plus the global `--json` and `--data-dir`. There is no Python +`vogt` in the stack image, so these Python commands are not in it: `status`, +`migrate`, `registry`, `diagnostics`, `engine`, `place`, `connect`, `mcp`, +`project`, `work`, `board`, `backlog`, `bugs`, `why`, `label`, `initiative`, +`actor`, `workflow`, `sweep`, `coverage`, `observations`, `deps`, `suppress`, +`suppression`, `contract`, `compliance`, `drift`, `session`, `decision`, +`agent-cli`, `agent-activity`, `token`, `auth`, `user`, `backup`, `restore`, +`clone`, `export`, `import`, `forge`, `events`, `notifications`, +`deployed-versions`, `inbox`, `preference`, `chat`, `audit`. `vogt-mcp` is the +engine's own stdio script, not the Python `mcp` command. Run those commands +against the Python core image (the root `Dockerfile`, published until I7), or +use the API and MCP endpoints. + **Upgrade.** 1. Take a backup. diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 4ba9f3f44..f4ac7b80d 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -102,16 +102,13 @@ the git routes. The merged image is built from the **repository root**, not from `engine/`: ```bash -docker build -f engine/Dockerfile \ - --build-arg CORE_IMAGE=ghcr.io/thedancingdeveloper-org/vogt:latest \ - -t vogt-engine . +docker build -f engine/Dockerfile -t vogt-engine . ``` The Dockerfile's stages are the PWA bundle, the Rust binary with that bundle -embedded, the published core image (lifted whole, so the merged image runs -*the* public core rather than a second build of it — `CORE_IMAGE` defaults to -the public `:latest` tag as a placeholder for a local build, and CI overrides -it with a digest), and the runtime. The runtime stage starts `FROM` a +embedded, and `vogt-core` built beside it from the same source (there is no +Python core in the image and no `CORE_IMAGE` argument), and the runtime. The +runtime stage starts `FROM` a **dev-pod base** (`engine/Dockerfile.pod`, published as `ghcr.io/thedancingdeveloper-org/vogt-pod-base:lean-*`): an Ubuntu image carrying the toolchains an agent working *inside* a session is likely to want @@ -124,9 +121,9 @@ built on it as `vogt-stack-workbench`; nothing in this repository deploys that image. It is a development pod rather than a hardened service image — it runs as a named user with `sudo`, with a writable home — which is why the core image at the -repository root is a separate build rather than a stage of this one: it is -the hardened input this image lifts its core from, not something to deploy on -its own (`DEPLOYMENT.md`). +repository root is a separate build rather than a stage of this one: it is the +hardened core-only image, not something to deploy on its own (`DEPLOYMENT.md`). +The merged image no longer takes its core from it; that image is retired at I7. Build arguments worth knowing: `INSTALL_AI_CLIENTS=true` bakes in the `claude` and `opencode` CLIs at the Renovate-pinned versions in diff --git a/engine/AGENTS.md b/engine/AGENTS.md index 81debdbcf..3231aa263 100644 --- a/engine/AGENTS.md +++ b/engine/AGENTS.md @@ -115,8 +115,8 @@ Things about the build that are easy to get wrong: `mkdir -p web/dist && touch web/dist/.placeholder` only as a guard. A production build must embed a real bundle. - `engine/Dockerfile` takes the repository root as its build context and - requires `CORE_IMAGE` (the published core image, by digest in CI) — it lifts - the core out of that image rather than building it a second time. + builds `vogt-core` in `server-build`, from the same source as the engine. It + takes no `CORE_IMAGE`; the root `Dockerfile` core image is retired at I7. - The runtime stage starts from `engine/Dockerfile.pod`, the dev-pod toolchain base, published separately and passed in by digest as `POD_BASE_IMAGE`. Toolchain changes go there; per-commit things (the agent CLIs) stay in diff --git a/scripts/rust_core_compose_smoke.sh b/scripts/rust_core_compose_smoke.sh index ecda3b399..af73ce0a6 100755 --- a/scripts/rust_core_compose_smoke.sh +++ b/scripts/rust_core_compose_smoke.sh @@ -50,7 +50,7 @@ echo "building the Rust core image" docker build -t vogt-core:rust -f deploy/rust-core.Dockerfile . echo "building the engine image with the Rust core in its core slot" -docker build -t vogt-engine:local -f engine/Dockerfile --build-arg CORE_IMAGE=vogt-core:rust . +docker build -t vogt-engine:local -f engine/Dockerfile . echo "starting the stack" "${COMPOSE[@]}" up -d diff --git a/tests/test_public_delivery.py b/tests/test_public_delivery.py index 8eaf5a7f2..6bf387e2a 100644 --- a/tests/test_public_delivery.py +++ b/tests/test_public_delivery.py @@ -53,7 +53,6 @@ def test_public_delivery_defaults_to_the_current_product_release() -> None: for path in ( PUBLIC_COMPOSE, PUBLIC_ENV, - ENGINE_OVERLAY, REPO_ROOT / "docs" / "CUSTOMISATION.md", REPO_ROOT / "docs" / "DEPLOYMENT.md", ): @@ -272,13 +271,13 @@ def test_the_engine_overlay_builds_the_engine_and_fronts_the_core() -> None: """An overlay states differences only, and this one adds the engine. No engine image is published, so the overlay always builds one from - `engine/Dockerfile`, lifting the published core in via `CORE_IMAGE`; it - then proxies to the sibling core by service name rather than running one. + `engine/Dockerfile`, which builds its own vogt-core; it then proxies to the + sibling core by service name rather than running one. """ overlay = _without_comments(ENGINE_OVERLAY.read_text(encoding="utf-8")) assert "engine:" in overlay assert "dockerfile: engine/Dockerfile" in overlay - assert "CORE_IMAGE:" in overlay + assert "CORE_IMAGE" not in overlay, "the engine builds its own core now" assert "INSTALL_AI_CLIENTS:" in overlay assert "CODEX_VERSION:" in overlay assert 'VOGT_CORE_URL: "http://vogt:8000"' in overlay From 4f110bf54060a41856f85c2cab9fdd4a2e4a5c00 Mon Sep 17 00:00:00 2001 From: sprooty Date: Sat, 17 Oct 2026 10:27:03 +0000 Subject: [PATCH 3/3] docs: describe the stack image's Rust admin CLI accurately (WI-1078) WI-1751 ported the admin verbs, so the stack image's vogt carries status, token issue, user create, backup, restore and project import/register. Only diagnostics, engine status and connect remain unported. Co-Authored-By: Claude Opus 5.5 --- docs/CUSTOMISATION.md | 31 ++++++++++++++++--------------- docs/DEPLOYMENT.md | 24 +++++++++++------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/docs/CUSTOMISATION.md b/docs/CUSTOMISATION.md index 5635e6701..1a9f08f8e 100644 --- a/docs/CUSTOMISATION.md +++ b/docs/CUSTOMISATION.md @@ -365,21 +365,22 @@ whatever its image declares — frequently an empty anonymous volume that Docker creates fresh on each deploy. So **administrative commands go to the core**: `project import`, -`project register`, `token issue`, `status`. These are Python-CLI verbs. The -core container here is the Python core image; the stack image has no Python -`vogt` and does not carry them (see [DEPLOYMENT.md](DEPLOYMENT.md), "The stack -image's CLI"). Anything you would run to -change what the instance knows belongs where the instance's state is. The -tempting `vogt init` that the error message suggests is exactly the wrong -response — it would initialise a second, empty instance in the container -that should not have one. - -One wrinkle if you go looking, in the Python core image: its CLI is inside a -virtualenv (`/opt/vogt/.venv/bin`) that is on `PATH` for a plain `docker exec` -but not for a *login* shell. The stack image has no virtualenv; its `vogt` is -the Rust binary, linked into `/usr/local/bin`. `docker exec core vogt status` finds it; -`docker exec core sh -lc 'vogt status'` may not, and reports -`vogt: not found` — a missing binary that is not missing. +`project register`, `token issue`, `user create`, `status`, `backup`, +`restore`. In the stack image the core is the Rust `vogt-core`, and its `vogt` +carries all of these (see [DEPLOYMENT.md](DEPLOYMENT.md), "The stack image's +CLI"); only `diagnostics`, `engine status` and `connect` are not yet ported. +Anything you would run to change what the instance knows belongs where the +instance's state is. The tempting `vogt init` that the error message suggests +is exactly the wrong response — it would initialise a second, empty instance +in the container that should not have one. + +One wrinkle if you go looking, in the Python core image (the root +`Dockerfile`, published until the swap): its CLI is inside a virtualenv +(`/opt/vogt/.venv/bin`) that is on `PATH` for a plain `docker exec` but not for +a *login* shell, so `docker exec core sh -lc 'vogt status'` may report +`vogt: not found` — a missing binary that is not missing. The stack image has +no virtualenv; its `vogt` is the Rust binary, linked into `/usr/local/bin`, +and is found either way. ## Layer 3 — extending the image diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index f5a120611..5bf8ddde1 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -535,19 +535,17 @@ run under a lock. Read [`SCHEMA.md`](SCHEMA.md) before a major version. **The stack image's CLI is the Rust core.** On the stack image's PATH, `vogt`, `vogt-core` and `vogt-mcp-remote` are one binary, `vogt-core`, built from -`engine/core` in the same build (WI-1078). It answers `init` (with `--check`) -and `serve`, plus the global `--json` and `--data-dir`. There is no Python -`vogt` in the stack image, so these Python commands are not in it: `status`, -`migrate`, `registry`, `diagnostics`, `engine`, `place`, `connect`, `mcp`, -`project`, `work`, `board`, `backlog`, `bugs`, `why`, `label`, `initiative`, -`actor`, `workflow`, `sweep`, `coverage`, `observations`, `deps`, `suppress`, -`suppression`, `contract`, `compliance`, `drift`, `session`, `decision`, -`agent-cli`, `agent-activity`, `token`, `auth`, `user`, `backup`, `restore`, -`clone`, `export`, `import`, `forge`, `events`, `notifications`, -`deployed-versions`, `inbox`, `preference`, `chat`, `audit`. `vogt-mcp` is the -engine's own stdio script, not the Python `mcp` command. Run those commands -against the Python core image (the root `Dockerfile`, published until I7), or -use the API and MCP endpoints. +`engine/core` in the same build (WI-1078). Besides `init` (with `--check`) and +`serve`, `vogt` carries the registry's CLI: the admin verbs `status`, +`token issue`, `user create`, `backup`, `restore`, `migrate`, `project import` +and `project register`, and the rest of the commands the Python CLI had +(`work`, `session`, `forge`, `export`, `import`, `clone`, ...), with the same +global `--json` and `--data-dir`. Run them in the container that holds the +store, for example `docker exec vogt token issue ...`. Three commands +are bound but not yet ported, and answer with a "not available" error rather +than an empty success: `diagnostics`, `engine status` and `connect`. Use the +API and MCP endpoints for those. `vogt-mcp` is the engine's own stdio script, +not the `mcp` command. **Upgrade.**