mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
Merge origin/main into bump-microsandbox-wb
Brings in main's image work (reproducible :latest manifest, version-keyed agent layers, WB diag CLIs + codestyle-pinned Python linters) alongside the 0.5.7 bump + 7-feature integration + agentd-teardown/fork-artifact fixes. Conflicts resolved in images/Dockerfile: - header/LABEL: describe the union (4 agents incl. Copilot + diag + codestyle). - agent installs reordered so Claude precedes the LSP-plugin block (the merge had stranded the LSP install ahead of `agent-vm-install claude`, so /root/.local/bin/claude was absent and the unguarded seed-stash hard-failed). - seed-stash made best-effort (skip when no plugins) to match the LSP block's soft-fail policy. - sanity check: keep main's hard-requirements + agent-CLI loop, add copilot. Verified: image builds (LSP ×4 installed + stashed, Copilot 1.0.64, sanity OK); boots with D1 copilot + D2 plugins √ enabled; teardown fix intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0119b4kV63pJXm5rGeZbz8jf
This commit is contained in:
commit
d3194bf831
3 changed files with 621 additions and 66 deletions
151
.github/workflows/build-image.yml
vendored
151
.github/workflows/build-image.yml
vendored
|
|
@ -44,10 +44,26 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Compute timestamp tag (UTC)
|
||||
- name: Compute timestamp tag + source epoch
|
||||
id: ts
|
||||
run: |
|
||||
# Immutable hourly pin (:YYYY-MM-DDTHH).
|
||||
echo "tag=$(date -u +%Y-%m-%dT%H)" >> "$GITHUB_OUTPUT"
|
||||
# SOURCE_DATE_EPOCH = HEAD commit time. Fed to buildkit (see
|
||||
# the build step), it clamps the image config's `created`
|
||||
# field and every history timestamp to a value that is
|
||||
# CONSTANT across the hourly cron builds of a given commit.
|
||||
# So two builds with the same commit + same agent versions +
|
||||
# same layers produce a byte-identical config → an identical
|
||||
# linux/amd64 manifest digest. microsandbox keys its
|
||||
# fsmeta/VMDK materialization on THAT digest, so an unchanged
|
||||
# `:latest` becomes a true no-op pull (0 bytes) instead of
|
||||
# re-downloading the whole image. A new commit, or any changed
|
||||
# agent layer, moves the digest as it should. Without this the
|
||||
# config carried a fresh build timestamp every hour, so every
|
||||
# cron `:latest` had a new manifest digest and consumers
|
||||
# re-pulled ~700 MiB of byte-identical layers hourly.
|
||||
echo "epoch=$(git log -1 --pretty=%ct)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
|
@ -59,8 +75,62 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Resolve agent versions
|
||||
id: ver
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
fail() { echo "::error::$1"; exit 1; }
|
||||
# Resolve each agent's current upstream version and feed it to
|
||||
# the Dockerfile as a per-agent cache key (AGENT_VERSION_*), so
|
||||
# an installer layer is rebuilt only when its agent actually
|
||||
# released — not on every hourly cron run. Each source is the
|
||||
# EXACT one the matching installer reads, so the key tracks
|
||||
# what actually gets installed:
|
||||
# - codex: openai/codex releases/latest tag — install.sh's
|
||||
# own resolver hits the same endpoint.
|
||||
# - opencode: anomalyco/opencode releases/latest tag — the
|
||||
# repo opencode.ai/install downloads from (it was
|
||||
# renamed from sst/opencode; sst/* only resolves
|
||||
# today via GitHub's rename redirect, so pin the
|
||||
# real repo).
|
||||
# - claude: downloads.claude.ai native channel `/latest`
|
||||
# (a plain version string) — what claude.ai/
|
||||
# install.sh installs. NOT npm: the npm dist-tags
|
||||
# (@anthropic-ai/claude-code) move on a separate
|
||||
# cadence (e.g. stable != latest), so keying on
|
||||
# npm would miss native releases / rebuild for
|
||||
# npm-only bumps.
|
||||
# Any lookup failure fails the build (vs shipping a stale key
|
||||
# that would skip a real agent update).
|
||||
codex=$(gh api repos/openai/codex/releases/latest --jq .tag_name) \
|
||||
|| fail "codex version lookup failed (openai/codex releases/latest)"
|
||||
opencode=$(gh api repos/anomalyco/opencode/releases/latest --jq .tag_name) \
|
||||
|| fail "opencode version lookup failed (anomalyco/opencode releases/latest)"
|
||||
claude=$(curl -fsSL https://downloads.claude.ai/claude-code-releases/latest) \
|
||||
|| fail "claude version lookup failed (downloads.claude.ai/claude-code-releases/latest)"
|
||||
# Guard against an HTTP-200-but-garbage response (empty body,
|
||||
# jq 'null', or an HTML error page) silently becoming a key.
|
||||
[ -n "$codex" ] && [ "$codex" != null ] || fail "codex version empty/null"
|
||||
[ -n "$opencode" ] && [ "$opencode" != null ] || fail "opencode version empty/null"
|
||||
case "$claude" in [0-9]*.[0-9]*.[0-9]*) : ;; *) fail "claude version implausible: '$claude'" ;; esac
|
||||
{
|
||||
echo "codex=$codex"
|
||||
echo "opencode=$opencode"
|
||||
echo "claude=$claude"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "resolved agent versions: codex=$codex opencode=$opencode claude=$claude"
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
env:
|
||||
# Clamps buildkit's image-config `created` + history
|
||||
# timestamps to the commit time (see "Compute timestamp tag +
|
||||
# source epoch") so an unchanged build yields an identical
|
||||
# manifest digest. docker/build-push-action forwards
|
||||
# SOURCE_DATE_EPOCH from the environment to buildkit.
|
||||
SOURCE_DATE_EPOCH: ${{ steps.ts.outputs.epoch }}
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: images
|
||||
|
|
@ -78,23 +148,51 @@ jobs:
|
|||
# past that for binary-heavy content.
|
||||
# microsandbox's `tar_ingest.rs:427` already accepts the
|
||||
# `application/vnd.oci.image.layer.v1.tar+zstd` media type.
|
||||
outputs: type=registry,push=true,compression=zstd,compression-level=3,force-compression=true
|
||||
# Cache-bust the three agent-installer RUN layers each run
|
||||
# by passing the per-build timestamp as a build-arg the
|
||||
# Dockerfile references right before those RUNs. Without
|
||||
# this, `cache-from: type=gha` matches the invariant
|
||||
# `RUN curl … install.sh | bash` instruction strings and
|
||||
# the hourly cron silently reused the same baked-in
|
||||
# agent binaries instead of fetching new releases (the
|
||||
# whole point of the schedule). The heavy apt/chromium/
|
||||
# docker/node layers above the ARG stay cached.
|
||||
#
|
||||
# `rewrite-timestamp=true` clamps the mtime of every file
|
||||
# INSIDE each layer tar to SOURCE_DATE_EPOCH (the commit time).
|
||||
# SOURCE_DATE_EPOCH alone only fixes the image config; layer
|
||||
# file mtimes default to build wall-clock, so a `cache-from`
|
||||
# MISS (gha eviction / size cap / a base-layer invalidation)
|
||||
# would re-run the apt/curl RUN steps and emit layers with
|
||||
# fresh mtimes → new diff_ids → new manifest digest for an
|
||||
# unchanged commit, re-introducing the ~700 MiB consumer
|
||||
# re-pull. Clamping mtimes makes the layer digests — and thus
|
||||
# the manifest digest — reproducible independent of cache
|
||||
# state, so the digest only moves on a real content change.
|
||||
outputs: type=registry,push=true,compression=zstd,compression-level=3,force-compression=true,rewrite-timestamp=true
|
||||
# Per-agent version cache keys. `Resolve agent versions`
|
||||
# above looks up each agent's current upstream version; the
|
||||
# Dockerfile references these in the matching installer RUN,
|
||||
# so a layer is rebuilt ONLY when that agent actually
|
||||
# released — not on every hourly cron run. This keeps an
|
||||
# unchanged hourly build a pure cache hit (identical
|
||||
# agent-layer digests → `:latest` consumers re-pull nothing)
|
||||
# instead of re-emitting ~200 MiB of fresh-but-identical
|
||||
# agent layers every hour. The heavy apt/chromium/docker/
|
||||
# node layers below the args stay cached regardless.
|
||||
build-args: |
|
||||
AGENT_INSTALL_CACHEBUST=${{ steps.ts.outputs.tag }}
|
||||
AGENT_VERSION_CODEX=${{ steps.ver.outputs.codex }}
|
||||
AGENT_VERSION_OPENCODE=${{ steps.ver.outputs.opencode }}
|
||||
AGENT_VERSION_CLAUDE=${{ steps.ver.outputs.claude }}
|
||||
# `cache-from`/`cache-to` keep layer rebuilds fast when only
|
||||
# the agent-version layers at the top of the Dockerfile
|
||||
# change (the common hourly case).
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# No provenance/SBOM attestation. Either one makes buildx wrap
|
||||
# even a single-platform build in an OCI index whose extra
|
||||
# attestation manifest embeds the build time, so the index
|
||||
# digest changes every build even when the image content is
|
||||
# identical — which would keep `:latest`'s digest churning
|
||||
# hourly (and nag the update banner) despite a stable
|
||||
# linux/amd64 manifest. We don't consume either anywhere.
|
||||
# Both are pinned off (provenance defaults to mode=min on push;
|
||||
# sbom defaults off but is pinned so a future default flip
|
||||
# can't silently re-introduce the index). The "Show pushed
|
||||
# digest" step asserts the push stayed a single manifest.
|
||||
provenance: false
|
||||
sbom: false
|
||||
# Moving tags (`:latest`, `:YYYY-MM-DDTHH`) are gated to
|
||||
# the integration branches (`main`, `rewrite-microsandbox`)
|
||||
# so workflow_dispatch on a feature branch can verify the
|
||||
|
|
@ -111,15 +209,40 @@ jobs:
|
|||
${{ env.IMAGE }}:sha-${{ github.sha }}
|
||||
# Tag with the short SHA too so we can trace any image
|
||||
# back to the exact Dockerfile commit.
|
||||
# NOTE: no `org.opencontainers.image.created` label. It was
|
||||
# set to the per-hour timestamp and lived in the image config,
|
||||
# so it changed the manifest digest every cron build even with
|
||||
# identical content (the root cause of the hourly re-pull). The
|
||||
# config still carries a `created` field, but clamped to the
|
||||
# commit time via SOURCE_DATE_EPOCH, so it's stable per commit.
|
||||
# `revision` (the commit sha) is stable across a commit's
|
||||
# hourly builds, so it doesn't reintroduce churn.
|
||||
labels: |
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
org.opencontainers.image.created=${{ steps.ts.outputs.tag }}
|
||||
|
||||
- name: Show pushed digest
|
||||
- name: Show pushed digest + assert single manifest
|
||||
run: |
|
||||
echo "Pushed ${{ env.IMAGE }}:${{ steps.ts.outputs.tag }}"
|
||||
echo " digest: ${{ steps.build.outputs.digest }}"
|
||||
# Guard the digest-stability invariant: the push must be a
|
||||
# single image manifest, NOT a multi-arch index. An index
|
||||
# (from a re-enabled provenance/sbom attestation, or a buildx
|
||||
# default flip) carries a build-time-varying attestation digest
|
||||
# and would silently restore the hourly `:latest` churn this
|
||||
# workflow exists to prevent. The immutable `:sha-…` tag is
|
||||
# always pushed (on every branch), so inspect that.
|
||||
ref="${IMAGE}:sha-${{ github.sha }}"
|
||||
if mt=$(docker buildx imagetools inspect --raw "$ref" 2>/dev/null | jq -r '.mediaType'); then
|
||||
echo " pushed mediaType: $mt"
|
||||
case "$mt" in
|
||||
*manifest.list*|*image.index*)
|
||||
echo "::error::pushed an index ($mt) — an attestation leaked back in; :latest digest will churn hourly. Ensure provenance:false + sbom:false."
|
||||
exit 1 ;;
|
||||
esac
|
||||
else
|
||||
echo "::warning::could not inspect $ref to assert single-manifest; skipping the check"
|
||||
fi
|
||||
|
||||
retain:
|
||||
# Delete date-tagged image versions older than 14 days so the
|
||||
|
|
|
|||
|
|
@ -4,10 +4,27 @@
|
|||
# @wirenboard/agent-vm on npm).
|
||||
#
|
||||
# Contents: Debian 13 slim + the four AI coding agents (Claude
|
||||
# Code, OpenCode, Codex, GitHub Copilot), minimum dev tooling, and the docker engine
|
||||
# (docker.io + containerd + runc + fuse-overlayfs). Other heavy
|
||||
# Code, OpenCode, Codex, GitHub Copilot), the docker engine
|
||||
# (docker.io + containerd + runc + fuse-overlayfs), network/diagnostic
|
||||
# CLIs, codestyle-pinned Python linters (black/isort/pylint matching
|
||||
# `wirenboard/codestyle`), and a clone of `wirenboard/codestyle`
|
||||
# at /opt/wirenboard/codestyle. Two heavier blocks (WB
|
||||
# build-essentials family + armhf/arm64 cross toolchains) are
|
||||
# present in the Dockerfile but COMMENTED OUT for the initial
|
||||
# roll-out — see the "DEFERRED" RUN blocks below. Other heavy
|
||||
# extras (LSPs, additional MCP servers, docker-buildx) are
|
||||
# deliberately deferred.
|
||||
# deliberately deferred too.
|
||||
#
|
||||
# Layer policy: the file is ordered so the LAST RUN steps are the
|
||||
# agent installers (the most frequently updated content). Each is
|
||||
# keyed on its agent's upstream version via a per-agent
|
||||
# `AGENT_VERSION_*` ARG (see below), so the hourly cron rebuilds an
|
||||
# installer layer ONLY when that agent actually released — an
|
||||
# unchanged build is a pure cache hit. Everything earlier — Debian
|
||||
# base, chromium, docker engine, Python lint pins, codestyle clone —
|
||||
# stays cached across rebuilds, so users pulling `:latest` re-download
|
||||
# only the layer(s) for whichever agent actually changed (and nothing
|
||||
# at all when none did).
|
||||
#
|
||||
# Published to ghcr.io/wirenboard/agent-vm-template:latest by the
|
||||
# hourly CI workflow (.github/workflows/build-image.yml). Source-
|
||||
|
|
@ -17,7 +34,7 @@
|
|||
FROM debian:13-slim
|
||||
|
||||
LABEL org.opencontainers.image.title="agent-vm guest template"
|
||||
LABEL org.opencontainers.image.description="Guest OCI image that the agent-vm tool (npm: @wirenboard/agent-vm) boots inside each per-project microVM. Contains Debian 13 + Claude Code + OpenCode + Codex + GitHub Copilot + minimal dev tooling. Rebuilt hourly to pick up new agent releases. NOT the agent-vm runtime itself — install that via `npm install -g @wirenboard/agent-vm`."
|
||||
LABEL org.opencontainers.image.description="Guest OCI image that the agent-vm tool (npm: @wirenboard/agent-vm) boots inside each per-project microVM. Contains Debian 13 + Claude Code + OpenCode + Codex + GitHub Copilot + diag tooling (ssh/ping/etc.) + codestyle-pinned Python linters + /opt/wirenboard/codestyle. Rebuilt hourly to pick up new agent releases. NOT the agent-vm runtime itself — install that via `npm install -g @wirenboard/agent-vm`."
|
||||
|
||||
# LANG=C.UTF-8: debian:slim ships with NO locale set, leaving the guest in
|
||||
# C/POSIX — which renders a non-ASCII (e.g. Cyrillic) cwd as `M-P…` meta-
|
||||
|
|
@ -35,10 +52,78 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
|||
# Image-API contract version. Bump on BREAKING image changes only
|
||||
# (new required mount points, changed env-var contracts, removed
|
||||
# binaries, renamed in-VM paths). Routine agent-version refreshes
|
||||
# do NOT change this. See `crates/agent-vm/src/defaults.rs` for the
|
||||
# matching range in the binary.
|
||||
# and tooling additions (build-essential, ssh, etc.) do NOT change
|
||||
# this — they're additive. See `crates/agent-vm/src/defaults.rs`
|
||||
# for the matching range in the binary.
|
||||
RUN echo 1 > /etc/agent-vm-image-version
|
||||
|
||||
# Conditionally trust a host-supplied CA bundle. Only fires when
|
||||
# `images/build.sh` is invoked on a host that sits behind a TLS-
|
||||
# intercept proxy (e.g. agent-vm-inside-agent-vm during local dev,
|
||||
# or a corporate egress MITM). The build script passes the host CA
|
||||
# via `--secret id=hostca` IF it exists at
|
||||
# `/usr/local/share/ca-certificates/microsandbox-ca.crt`. In CI
|
||||
# (no such file → no secret → empty mount), the `-s` test fails
|
||||
# and this is a no-op — the production image is unchanged.
|
||||
#
|
||||
# `--mount=type=secret` keeps the CA file in the buildkit secret
|
||||
# store, not in the layer; what IS persisted is the merged
|
||||
# /etc/ssl/certs/ca-certificates.crt. That's fine: the launcher
|
||||
# re-imports its per-boot CA at /usr/local/share/ca-certificates/
|
||||
# at runtime anyway, so a stale build-time CA in the bundle is
|
||||
# inert (and removed if a user runs update-ca-certificates).
|
||||
#
|
||||
# `CA_SHIM_CACHEBUST`: buildkit does NOT include secret content in
|
||||
# the RUN cache key (by design — secrets shouldn't bake into
|
||||
# images). So toggling secret presence across builds doesn't
|
||||
# invalidate this layer on its own. `images/build.sh` passes the
|
||||
# CA file's mtime (`stat -c %Y …`) when the secret is included, so
|
||||
# any change to the CA file (or going from "secret absent" → "secret
|
||||
# present") shifts the ARG and rebuilds. In CI (no secret, empty
|
||||
# arg) the layer also no-ops and stays cached.
|
||||
ARG CA_SHIM_CACHEBUST=
|
||||
RUN --mount=type=secret,id=hostca,target=/tmp/hostca.crt,required=false \
|
||||
: "${CA_SHIM_CACHEBUST}" \
|
||||
&& if [ -s /tmp/hostca.crt ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& install -d /usr/local/share/ca-certificates \
|
||||
&& install -m 0644 /tmp/hostca.crt /usr/local/share/ca-certificates/agent-vm-build.crt \
|
||||
&& update-ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& echo "==> host CA trusted for this build"; \
|
||||
fi
|
||||
|
||||
# Base apt — interpreters, fetchers, search/text utilities, AND
|
||||
# network/diagnostic CLIs. Bundling these into the existing base
|
||||
# layer (instead of spinning up a new layer for "ssh + ping") keeps
|
||||
# the layer count down: every additional layer costs a small
|
||||
# per-layer materialize overhead in microsandbox even when nothing
|
||||
# changes, so we only earn a new layer when we can articulate why
|
||||
# it'd be invalidated independently.
|
||||
#
|
||||
# Network/diag tools rationale (engineers debug WB devices over
|
||||
# SSH and serial; the in-VM agent often needs to confirm a host is
|
||||
# reachable before chasing logs):
|
||||
# - iputils-ping, iputils-tracepath, traceroute, mtr-tiny:
|
||||
# reachability + per-hop latency.
|
||||
# - net-tools: legacy `ifconfig`, `netstat`, `route` — still
|
||||
# muscle-memory for most embedded engineers, and many WB docs
|
||||
# reference them by name.
|
||||
# - iproute2: modern `ip`, `ss`. Usually preinstalled, listed
|
||||
# explicitly so an upstream slimming doesn't silently drop it.
|
||||
# - openssh-client: `ssh`, `scp`, `sftp`, `ssh-keygen`. No server
|
||||
# — the agent should never accept inbound connections.
|
||||
# - sshpass: scripted password-auth SSH (lab fixtures, factory
|
||||
# test rigs); also used by some WB CI scripts.
|
||||
# - dnsutils: `dig`, `nslookup`, `host`.
|
||||
# - netcat-openbsd: `nc` for ad-hoc TCP/UDP poking.
|
||||
# - tcpdump: rare but invaluable when an HTTP MITM looks wrong.
|
||||
# - gnupg: needed below to add the GitHub CLI apt key (and any
|
||||
# user-side apt sources `.agent-vm.runtime.sh` adds).
|
||||
# - less, vim-tiny, file, unzip, zip, rsync, htop, lsof, strace,
|
||||
# tmux: standard "I'm in a shell, I need a tool" muscle-memory
|
||||
# set. Each is <5 MB; the bundle is well under 30 MB.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
|
|
@ -48,6 +133,13 @@ RUN apt-get update \
|
|||
bash \
|
||||
python3 python3-pip \
|
||||
ripgrep fd-find \
|
||||
gnupg \
|
||||
iputils-ping iputils-tracepath traceroute mtr-tiny \
|
||||
net-tools iproute2 \
|
||||
openssh-client sshpass \
|
||||
dnsutils netcat-openbsd tcpdump \
|
||||
less vim-tiny file unzip zip rsync \
|
||||
htop lsof strace tmux \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Chromium for the chrome-devtools MCP server (Phase 7). The MCP
|
||||
|
|
@ -183,27 +275,6 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Pre-warm chrome-devtools-mcp's npm cache as the chrome user so the
|
||||
# first browser tool call in any sandbox doesn't pay the multi-second
|
||||
# `npx -y` fetch cost. `--help` exits cleanly after install/version-
|
||||
# check; it's the cheapest exercise of the full install path.
|
||||
#
|
||||
# Pinned to a known-good version. The runtime MCP config in
|
||||
# `crates/agent-vm/src/secrets.rs::write_default_claude_root_state`
|
||||
# pins the SAME version — bump both together. Without the pin the
|
||||
# cache stops being useful the first time upstream cuts a release:
|
||||
# `npx -y` re-resolves `@latest` against the registry and re-
|
||||
# downloads under the writable upper layer.
|
||||
#
|
||||
# Best-effort: `|| true` because npm-registry transient failure or a
|
||||
# bad `--help` exit code shouldn't fail the whole `agent-vm setup`.
|
||||
# A missing cache costs a multi-second fetch on first browser tool
|
||||
# call; failing the image build is much worse.
|
||||
#
|
||||
# Must come AFTER the nodejs RUN above — npx didn't exist before it.
|
||||
RUN sudo -u chrome -H npx -y chrome-devtools-mcp@1.0.1 --help >/dev/null 2>&1 \
|
||||
|| echo "==> pre-warm skipped (will lazy-fetch chrome-devtools-mcp at first MCP call)"
|
||||
|
||||
# Docker engine — dockerd + cli + containerd + runc.
|
||||
#
|
||||
# Why each piece:
|
||||
|
|
@ -243,19 +314,294 @@ RUN apt-get update \
|
|||
&& printf '%s\n' '{' ' "storage-driver": "fuse-overlayfs"' '}' \
|
||||
> /etc/docker/daemon.json
|
||||
|
||||
# Cache-bust for the three agent installers below. The workflow
|
||||
# passes the hourly timestamp tag as the value so each scheduled
|
||||
# build invalidates exactly these RUN layers (and the sanity check
|
||||
# that follows) while leaving the heavy apt/chromium/docker/node
|
||||
# layers cached. Without this the GHA layer cache reuses the
|
||||
# `curl … install.sh | bash` layers indefinitely, so hourly cron
|
||||
# runs never picked up new claude-code/codex/opencode releases.
|
||||
# Local `images/build.sh` builds leave the default empty value and
|
||||
# get the normal Docker layer-cache behaviour.
|
||||
ARG AGENT_INSTALL_CACHEBUST=
|
||||
# Pre-warm chrome-devtools-mcp's npm cache as the chrome user so the
|
||||
# first browser tool call in any sandbox doesn't pay the multi-second
|
||||
# `npx -y` fetch cost. `--help` exits cleanly after install/version-
|
||||
# check; it's the cheapest exercise of the full install path.
|
||||
#
|
||||
# Pinned to a known-good version. The runtime MCP config in
|
||||
# `crates/agent-vm/src/secrets.rs::write_default_claude_root_state`
|
||||
# pins the SAME version — bump both together. Without the pin the
|
||||
# cache stops being useful the first time upstream cuts a release:
|
||||
# `npx -y` re-resolves `@latest` against the registry and re-
|
||||
# downloads under the writable upper layer.
|
||||
#
|
||||
# Best-effort: `|| true` because npm-registry transient failure or a
|
||||
# bad `--help` exit code shouldn't fail the whole `agent-vm setup`.
|
||||
# A missing cache costs a multi-second fetch on first browser tool
|
||||
# call; failing the image build is much worse.
|
||||
#
|
||||
# Must come AFTER the nodejs RUN above — npx didn't exist before it.
|
||||
RUN sudo -u chrome -H npx -y chrome-devtools-mcp@1.0.1 --help >/dev/null 2>&1 \
|
||||
|| echo "==> pre-warm skipped (will lazy-fetch chrome-devtools-mcp at first MCP call)"
|
||||
|
||||
# Claude Code official installer.
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
# Wiren Board C/C++ build essentials — DEFERRED.
|
||||
#
|
||||
# These two RUN blocks (build-essential family + armhf/arm64 cross
|
||||
# toolchains) are commented out for the initial roll-out. They
|
||||
# account for ~466 MiB compressed (~1.6 GB uncompressed) of the
|
||||
# original image size estimate; deferring them keeps the first
|
||||
# tooling-image bump lean while the layer ordering / cron-cache
|
||||
# wins land. Uncomment to re-enable when WB engineers actually
|
||||
# need the cross-compile path in-VM (the existing host
|
||||
# `schroot -c bullseye-amd64-sbuild …` workflow still works).
|
||||
#
|
||||
# Sourced from the union of debian/control Build-Depends across
|
||||
# wb-mqtt-serial, wb-mqtt-mbgate, wb-rules, wb-mqtt-confed and the
|
||||
# `cpp/local/linux-devenv.sh` / `cpp/ci/clang-*-subdirs.sh` scripts
|
||||
# in `wirenboard/codestyle`. When uncommenting, also re-add the
|
||||
# corresponding sanity-check probes for clang-format,
|
||||
# arm-linux-gnueabihf-gcc, aarch64-linux-gnu-gcc, and
|
||||
# qemu-arm-static at the bottom of the Dockerfile.
|
||||
#
|
||||
# - build-essential: gcc/g++/make/dpkg-dev metapackage.
|
||||
# - debhelper, devscripts, equivs, dpkg-cross: Debian packaging
|
||||
# primitives. `debhelper (>= 10)` is mandated by every WB
|
||||
# debian/control; `equivs` is required for `mk-build-deps -ir`
|
||||
# (the recommended `linux-devenv.sh` install path).
|
||||
# - pkg-config: standard build-time discovery.
|
||||
# - clang-format, clang-tidy: the only formatters/linters WB
|
||||
# codestyle ships configs for (`cpp/config/.clang-format`,
|
||||
# `.clang-tidy`). `cpp/ci/clang-format-subdirs.sh` and
|
||||
# `clang-tidy-subdirs.sh` call them directly.
|
||||
# - libcurl4-openssl-dev, libgtest-dev, libgmock-dev,
|
||||
# libmodbus-dev, libsystemd-dev: most-frequent C/C++ deps
|
||||
# across the WB services (modbus serial gateway, MQTT bridge,
|
||||
# etc).
|
||||
# - gcovr: pinned in `python/vscode/.devcontainer/devcontainer.json`
|
||||
# via the `coverage-gutters` extension; wb-mqtt-serial's
|
||||
# Build-Depends lists it explicitly.
|
||||
# - gdb-multiarch: install-listed by `cpp/local/linux-devenv.sh`;
|
||||
# debugging cross-built armhf/arm64 binaries.
|
||||
# - cmake, ninja-build: not in every WB repo but common enough
|
||||
# (firmware repo uses cmake) that excluding them would catch
|
||||
# people off guard.
|
||||
# - python3-cups, python3-libgpiod, python3-pycurl: per the
|
||||
# allowed-c-extensions list in `python/config/pyproject.toml`,
|
||||
# so pylint can resolve imports in WB Python projects without
|
||||
# spurious E0401.
|
||||
# RUN apt-get update \
|
||||
# && apt-get install -y --no-install-recommends \
|
||||
# build-essential \
|
||||
# debhelper devscripts equivs dpkg-cross \
|
||||
# pkg-config \
|
||||
# clang-format clang-tidy \
|
||||
# libcurl4-openssl-dev \
|
||||
# libgtest-dev libgmock-dev \
|
||||
# libmodbus-dev \
|
||||
# libsystemd-dev \
|
||||
# gcovr \
|
||||
# gdb-multiarch \
|
||||
# cmake ninja-build \
|
||||
# python3-cups python3-libgpiod python3-pycurl \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Wiren Board cross-build toolchains — armhf + arm64 — DEFERRED.
|
||||
#
|
||||
# See the deferment note on the build-essentials block above.
|
||||
#
|
||||
# Every flagship WB binary ships for one of these two architectures
|
||||
# (controllers are armhf, newer hardware is arm64); the canonical
|
||||
# build command in `codestyle/cpp/vscode/.vscode/tasks.json` is
|
||||
# `schroot -c bullseye-amd64-sbuild -- DEB_HOST_MULTIARCH=arm-…
|
||||
# make -j12`, and qemu-user-static is what makes the resulting
|
||||
# binary runnable on x86_64 for tests.
|
||||
#
|
||||
# Kept in a SEPARATE layer from the C/C++ essentials above because
|
||||
# it's the single biggest discretionary addition (~300 MB
|
||||
# uncompressed / ~258 MiB compressed). A future Dockerfile.slim
|
||||
# can keep this commented while uncommenting the build-essentials
|
||||
# block above.
|
||||
#
|
||||
# - crossbuild-essential-armhf, crossbuild-essential-arm64:
|
||||
# gcc-arm-linux-gnueabihf / gcc-aarch64-linux-gnu meta-packages.
|
||||
# - qemu-user-static: registers binfmt handlers so an `armhf` or
|
||||
# `arm64` ELF executes under qemu transparently. Required by
|
||||
# the `[armhf]/[arm64] Run tests for debug` tasks in codestyle.
|
||||
# - debootstrap, schroot, sbuild: the canonical WB build path is
|
||||
# `sbuild` inside a `schroot` named `bullseye-amd64-sbuild`.
|
||||
# The chroot itself is NOT created here (it's per-host, lives
|
||||
# at /srv/chroot, and requires the schroot config; the user
|
||||
# creates it on first need with `sbuild-createchroot`).
|
||||
# RUN apt-get update \
|
||||
# && apt-get install -y --no-install-recommends \
|
||||
# crossbuild-essential-armhf \
|
||||
# crossbuild-essential-arm64 \
|
||||
# qemu-user-static \
|
||||
# debootstrap schroot sbuild \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Python lint/format toolchain — pinned to the versions in
|
||||
# `wirenboard/codestyle/python/config/requirements.txt`. Pinning
|
||||
# matters: codestyle ships a `pyproject.toml` that engineers copy
|
||||
# into projects, and a black/isort/pylint version skew between
|
||||
# image and project re-formats files differently → noisy diffs.
|
||||
#
|
||||
# `--break-system-packages` because Debian 13 enforces PEP 668 and
|
||||
# we WANT these system-wide so any in-VM tool finds them. The
|
||||
# microVM rootfs is throwaway, so there's no system Python we can
|
||||
# accidentally damage. `--no-cache-dir` shaves ~40 MB off the
|
||||
# layer (the pip download cache is dead weight in an image).
|
||||
#
|
||||
# Versions sourced from
|
||||
# `codestyle/python/config/requirements.txt` (master). `attrs==23.1.0`
|
||||
# is per the workaround in `codestyle/python/local/linux-devenv.sh`
|
||||
# (newer attrs breaks the pinned pylint). `j2cli` is in
|
||||
# wb-mqtt-serial's Build-Depends and is a one-line install here.
|
||||
RUN pip3 install --no-cache-dir --break-system-packages \
|
||||
black==24.2.0 \
|
||||
isort==5.13.2 \
|
||||
pylint==3.3.9 \
|
||||
pytest-cov==2.10.1 \
|
||||
attrs==23.1.0 \
|
||||
j2cli
|
||||
|
||||
# Shallow-clone `wirenboard/codestyle` so the per-project
|
||||
# devenv scripts work without each project re-cloning it.
|
||||
#
|
||||
# The script `cpp/local/linux-devenv.sh` and its Python sibling
|
||||
# both expect to be invoked from inside a project as
|
||||
# `bash ../codestyle/{cpp,python}/local/linux-devenv.sh`, i.e.
|
||||
# with codestyle as a sibling directory. Having it at a known
|
||||
# path inside the image lets a project hook (`.agent-vm.runtime.sh`)
|
||||
# symlink/copy it into place at launch instead of cloning over
|
||||
# the proxy on every cold start.
|
||||
#
|
||||
# `CODESTYLE_REF` build-arg (default `master`) lets a reproducible
|
||||
# build pin to a specific ref — `git clone --branch` accepts a
|
||||
# branch or tag name. The hourly CI cron just builds from master,
|
||||
# matching the previous behavior.
|
||||
#
|
||||
# Hard-fails by default: if GitHub is unreachable the resulting
|
||||
# image is missing the devenv path that several WB workflows rely
|
||||
# on, and we'd rather not silently ship that. Source-checkout dev
|
||||
# builds that set `AGENT_INSTALL_SOFT_FAIL=1` (see images/build.sh,
|
||||
# auto-set on TLS-intercept hosts) downgrade to a warning.
|
||||
#
|
||||
# `AGENT_INSTALL_SOFT_FAIL` is also consumed by the three agent
|
||||
# installers + the sanity check below; declared here at first use
|
||||
# so a single toggle covers everything that might legitimately fail
|
||||
# in a dev sandbox but must succeed in CI.
|
||||
ARG CODESTYLE_REF=master
|
||||
ARG AGENT_INSTALL_SOFT_FAIL=
|
||||
RUN git clone --depth=1 --branch "${CODESTYLE_REF}" \
|
||||
https://github.com/wirenboard/codestyle.git \
|
||||
/opt/wirenboard/codestyle \
|
||||
|| { msg="==> wirenboard/codestyle clone FAILED (ref=${CODESTYLE_REF}) — re-clone in-VM with: git clone https://github.com/wirenboard/codestyle.git /opt/wirenboard/codestyle"; \
|
||||
if [ -n "${AGENT_INSTALL_SOFT_FAIL:-}" ]; then echo "$msg (soft-fail mode)"; else echo "$msg" >&2; exit 1; fi; }
|
||||
|
||||
# Per-agent cache keys (the three `ARG AGENT_VERSION_*` below, one
|
||||
# per installer RUN). The workflow resolves each agent's current
|
||||
# UPSTREAM version and passes it in, so an installer layer is rebuilt
|
||||
# only when that agent actually released — not on every hourly cron
|
||||
# run.
|
||||
#
|
||||
# This replaces the old single `AGENT_INSTALL_CACHEBUST=<timestamp>`
|
||||
# arg, which changed every hour and re-emitted all three
|
||||
# `curl … install.sh | bash` layers (plus the sanity check) on every
|
||||
# build. Because those installs aren't reproducible, each rebuild
|
||||
# produced fresh layer blobs → a brand-new ~200 MiB-of-agent-layers
|
||||
# image every hour even when no agent version had changed, so
|
||||
# `:latest` consumers re-downloaded those layers hourly for nothing.
|
||||
# Keying on the real version makes an unchanged hourly build a pure
|
||||
# cache hit (identical layer digests → nothing to re-pull). Local
|
||||
# `images/build.sh` builds leave the defaults empty and get normal
|
||||
# Docker layer-cache behaviour.
|
||||
|
||||
# Shared helper for the three agent installer RUNs below. Downloads
|
||||
# the upstream `install.sh`, runs it under the requested shell, and
|
||||
# emits a uniform soft-fail or hard-fail diagnostic depending on
|
||||
# `AGENT_INSTALL_SOFT_FAIL`. Keeping the policy in one place means
|
||||
# any future tweak (extra retries, log format, soft-fail discrimina-
|
||||
# tion) lives in exactly one spot — the three RUNs that call it are
|
||||
# trivial one-liners.
|
||||
#
|
||||
# Why curl-only (no python3-urllib fallback): both curl and python3's
|
||||
# `ssl` module link against the same system OpenSSL, so a real TLS-
|
||||
# layer failure (the curl 56 "SSL_read: unexpected eof" we see from
|
||||
# inside MITM-proxied dev sandboxes) hits the fallback identically.
|
||||
# `--retry 5 --retry-all-errors --http1.1` covers the transient cases
|
||||
# that ARE recoverable. The MITM-sandbox case is what
|
||||
# `AGENT_INSTALL_SOFT_FAIL=1` exists for.
|
||||
#
|
||||
# Written via `printf '%s\n'` because debian-slim's `/bin/sh` is
|
||||
# POSIX dash, no heredoc niceties. The script is small enough that
|
||||
# adding a layer for it is essentially free (well under 1 KB).
|
||||
RUN printf '%s\n' \
|
||||
'#!/bin/sh' \
|
||||
'# agent-vm-install <name> <shell> <url>' \
|
||||
'# <name> - agent identifier (codex, opencode, claude)' \
|
||||
'# <shell> - sh|bash; the interpreter used to run the installer' \
|
||||
'# <url> - upstream installer URL' \
|
||||
'# Honors $AGENT_INSTALL_SOFT_FAIL: when non-empty, a download or' \
|
||||
'# install failure becomes a warning + exit 0 instead of exit 1.' \
|
||||
'set -eu' \
|
||||
'name=$1; shell=$2; url=$3' \
|
||||
'tmp=$(mktemp)' \
|
||||
'trap "rm -f \"$tmp\"" EXIT INT TERM' \
|
||||
'if curl -fsSL --retry 5 --retry-all-errors --http1.1 "$url" -o "$tmp" \' \
|
||||
' && "$shell" "$tmp"; then' \
|
||||
' exit 0' \
|
||||
'fi' \
|
||||
'msg="==> $name installer FAILED — install in-VM with: curl -fsSL $url | $shell"' \
|
||||
'if [ -n "${AGENT_INSTALL_SOFT_FAIL:-}" ]; then' \
|
||||
' echo "$msg (soft-fail mode; image will ship without $name)"' \
|
||||
' exit 0' \
|
||||
'fi' \
|
||||
'echo "$msg" >&2' \
|
||||
'exit 1' \
|
||||
> /usr/local/bin/agent-vm-install \
|
||||
&& chmod 0755 /usr/local/bin/agent-vm-install \
|
||||
&& test -s /usr/local/bin/agent-vm-install
|
||||
|
||||
# The three agents each get their own RUN + their own version arg so
|
||||
# a single agent's release rebuilds only that layer (and, by Docker's
|
||||
# layer cascade, the layers stacked ABOVE it).
|
||||
#
|
||||
# Layer order is deliberate and is NOT by size. A change to any layer
|
||||
# forces every layer above it to rebuild, so the TOPMOST agent layer
|
||||
# is re-emitted on essentially every build that changes anything,
|
||||
# while the BOTTOM agent layer is re-emitted only when it itself
|
||||
# bumps. The agent that is both largest and most frequently released
|
||||
# therefore belongs at the BOTTOM, not the top:
|
||||
# - codex ~95 MiB, multiple stable cuts/day → bottom (first)
|
||||
# - opencode ~50 MiB, several per week → middle
|
||||
# - claude ~68 MiB, ~daily → top (last)
|
||||
# This minimizes expected re-pushed bytes per changed build across
|
||||
# the plausible release-frequency range; putting the big codex layer
|
||||
# on top (so it re-pushes on every change) is the worst case. Each
|
||||
# RUN references its version arg (`: "${AGENT_VERSION_*}"`) so
|
||||
# BuildKit ties the layer's cache key to the version string and
|
||||
# rebuilds it ONLY when that string changes.
|
||||
#
|
||||
# Agent installers HARD-FAIL by default — if an upstream installer
|
||||
# is broken or removed, the hourly cron should NOT silently ship an
|
||||
# image missing an agent. `AGENT_INSTALL_SOFT_FAIL=1` (build-arg,
|
||||
# auto-set by images/build.sh on TLS-intercept dev hosts; CI never
|
||||
# sets it) turns the three RUNs into warnings instead.
|
||||
|
||||
# Codex CLI official installer.
|
||||
ARG AGENT_VERSION_CODEX=
|
||||
RUN : "codex ${AGENT_VERSION_CODEX}" \
|
||||
&& agent-vm-install codex sh \
|
||||
https://github.com/openai/codex/releases/latest/download/install.sh
|
||||
|
||||
# OpenCode official installer. Installs into ~/.opencode/bin; PATH already
|
||||
# includes it. Symlink into /usr/local/bin so non-login shells find it too.
|
||||
# The symlink is best-effort — under soft-fail mode the install may have
|
||||
# been skipped, in which case there's nothing to link.
|
||||
ARG AGENT_VERSION_OPENCODE=
|
||||
RUN : "opencode ${AGENT_VERSION_OPENCODE}" \
|
||||
&& agent-vm-install opencode bash https://opencode.ai/install \
|
||||
&& { [ -x /root/.opencode/bin/opencode ] \
|
||||
&& ln -sf /root/.opencode/bin/opencode /usr/local/bin/opencode \
|
||||
|| true; }
|
||||
|
||||
# Claude Code official installer. MUST precede the LSP-plugin install below,
|
||||
# which invokes /root/.local/bin/claude.
|
||||
ARG AGENT_VERSION_CLAUDE=
|
||||
RUN : "claude ${AGENT_VERSION_CLAUDE}" \
|
||||
&& agent-vm-install claude bash https://claude.ai/install.sh
|
||||
|
||||
# Claude Code LSP plugins (C/C++, Python, TypeScript, Go) baked in so
|
||||
# in-VM Claude has code intelligence on first launch. Ports the four
|
||||
|
|
@ -308,20 +654,21 @@ RUN { /root/.local/bin/claude plugin marketplace add anthropics/claude-plugins-o
|
|||
# booted guest's `claude plugin list` shows nothing. Stash the plugins +
|
||||
# settings to a NON-shadowed path and ship a first-boot seed script; the
|
||||
# launcher prelude calls it before exec'ing the agent.
|
||||
# Best-effort like the LSP install above: if a transient blip skipped the
|
||||
# plugins, stash nothing rather than hard-failing the image — the first-boot
|
||||
# seed then becomes a no-op and plugins lazy-install at runtime.
|
||||
RUN mkdir -p /opt/agent-vm/claude-seed \
|
||||
&& cp -a /root/.claude/plugins /opt/agent-vm/claude-seed/plugins \
|
||||
&& cp -a /root/.claude/settings.json /opt/agent-vm/claude-seed/settings.json
|
||||
&& if [ -d /root/.claude/plugins ]; then \
|
||||
cp -a /root/.claude/plugins /opt/agent-vm/claude-seed/plugins; \
|
||||
[ -f /root/.claude/settings.json ] \
|
||||
&& cp -a /root/.claude/settings.json /opt/agent-vm/claude-seed/settings.json; \
|
||||
echo "==> stashed claude LSP plugins for first-boot seeding"; \
|
||||
else \
|
||||
echo "==> no /root/.claude/plugins to stash (LSP install skipped); D2 seed is a no-op"; \
|
||||
fi
|
||||
COPY seed-claude-plugins.sh /opt/agent-vm/seed-claude-plugins.sh
|
||||
RUN chmod +x /opt/agent-vm/seed-claude-plugins.sh
|
||||
|
||||
# OpenCode official installer. Installs into ~/.opencode/bin; PATH already
|
||||
# includes it. Symlink into /usr/local/bin so non-login shells find it too.
|
||||
RUN curl -fsSL https://opencode.ai/install | bash \
|
||||
&& ln -sf /root/.opencode/bin/opencode /usr/local/bin/opencode
|
||||
|
||||
# Codex CLI official installer.
|
||||
RUN curl -fsSL https://github.com/openai/codex/releases/latest/download/install.sh | sh
|
||||
|
||||
# D1: GitHub Copilot CLI. Its own RUN layer so the AGENT_INSTALL_CACHEBUST
|
||||
# arg above invalidates it on the hourly build like the other agents, and
|
||||
# so a wrong package/binary name fails the image build loudly (via the
|
||||
|
|
@ -329,9 +676,49 @@ RUN curl -fsSL https://github.com/openai/codex/releases/latest/download/install.
|
|||
# command-not-found inside the guest at `agent-vm copilot` time.
|
||||
RUN npm install -g @github/copilot && copilot --version
|
||||
|
||||
# Sanity check at build time so a broken installer surfaces before we push.
|
||||
RUN claude --version && opencode --version && codex --version && copilot --version \
|
||||
&& dockerd --version && docker --version && containerd --version && runc --version
|
||||
# Sanity check at build time. Every command is invoked directly
|
||||
# (`tool --version >/dev/null`) so a present-but-broken binary —
|
||||
# corrupt download, broken symlink target, missing libc symbol —
|
||||
# fails the build instead of slipping through a `command -v` exists-
|
||||
# check. `set -e` propagates the first failure.
|
||||
#
|
||||
# Agent CLIs (claude/opencode/codex) are HARD-required unless
|
||||
# `AGENT_INSTALL_SOFT_FAIL` is set, in which case they're SOFT
|
||||
# (missing → warning, not failure) — matches the policy on the
|
||||
# installer RUNs above. /opt/wirenboard/codestyle follows the same
|
||||
# policy (its clone RUN above also honors AGENT_INSTALL_SOFT_FAIL).
|
||||
RUN set -e \
|
||||
&& echo "== HARD requirements ==" \
|
||||
&& dockerd --version >/dev/null \
|
||||
&& docker --version >/dev/null \
|
||||
&& containerd --version >/dev/null \
|
||||
&& runc --version >/dev/null \
|
||||
&& black --version >/dev/null \
|
||||
&& isort --version >/dev/null \
|
||||
&& pylint --version >/dev/null \
|
||||
&& ssh -V >/dev/null 2>&1 \
|
||||
&& sshpass -V >/dev/null 2>&1 \
|
||||
&& ping -V >/dev/null 2>&1 \
|
||||
&& { if [ -d /opt/wirenboard/codestyle ]; then \
|
||||
echo " codestyle clone: OK"; \
|
||||
elif [ -n "${AGENT_INSTALL_SOFT_FAIL:-}" ]; then \
|
||||
echo " codestyle clone: MISSING (soft-fail mode)"; \
|
||||
else \
|
||||
echo " codestyle clone: MISSING — hard sanity failure" >&2; exit 1; \
|
||||
fi; } \
|
||||
&& echo "== agent CLIs ==" \
|
||||
&& for bin in claude opencode codex copilot; do \
|
||||
if "$bin" --version >/dev/null 2>&1; then \
|
||||
ver=$("$bin" --version 2>&1 | head -1); \
|
||||
echo " $bin: $ver"; \
|
||||
elif [ -n "${AGENT_INSTALL_SOFT_FAIL:-}" ]; then \
|
||||
echo " $bin: MISSING (soft-fail mode)"; \
|
||||
else \
|
||||
echo " $bin: MISSING — hard sanity failure" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done \
|
||||
&& echo "== sanity OK =="
|
||||
|
||||
# Smoke entrypoint; the launcher overrides this in Phase 2.
|
||||
CMD ["/bin/bash"]
|
||||
|
|
|
|||
|
|
@ -125,8 +125,53 @@ build_and_push() {
|
|||
# win. `registry.insecure=true` lets us push to the loopback HTTP
|
||||
# registry. We use `compression-level=3` (zstd's default) — the
|
||||
# bench shows diminishing returns past that for binary-heavy layers.
|
||||
|
||||
# If the host is itself behind a TLS-intercept proxy (agent-vm-
|
||||
# inside-agent-vm during local dev, or a corporate egress MITM),
|
||||
# the buildkit container's outbound HTTPS sees the proxy's CA and
|
||||
# curl/apt fail with "unable to verify the legitimacy of the
|
||||
# server". Detect the host CA and:
|
||||
# - pass it as a buildx secret (the Dockerfile imports it
|
||||
# conditionally; no-op when the secret is absent),
|
||||
# - key the RUN-cache invalidation off its mtime (CA_SHIM_
|
||||
# CACHEBUST — see Dockerfile comment for why secret content
|
||||
# alone doesn't invalidate the cache),
|
||||
# - run the RUN steps in the host network namespace, because
|
||||
# buildkit's default bridge stack drops some HTTPS connec-
|
||||
# tions (curl 56 `SSL_read: unexpected eof`) mid-redirect
|
||||
# through the MITM proxy.
|
||||
# Production CI has no such host CA and skips all of this.
|
||||
local extra=()
|
||||
local host_ca="${AGENT_VM_BUILD_HOST_CA:-/usr/local/share/ca-certificates/microsandbox-ca.crt}"
|
||||
local mitm_detected=
|
||||
if [ -f "${host_ca}" ]; then
|
||||
echo "==> Including host CA ${host_ca} as buildx secret (TLS-intercept proxy detected)"
|
||||
extra+=(--secret "id=hostca,src=${host_ca}")
|
||||
extra+=(--build-arg "CA_SHIM_CACHEBUST=$(stat -c %Y "${host_ca}")")
|
||||
extra+=(--allow "network.host")
|
||||
extra+=(--network "host")
|
||||
mitm_detected=1
|
||||
fi
|
||||
|
||||
# AGENT_INSTALL_SOFT_FAIL — independent toggle (not tied to CA
|
||||
# detection) so a clean-network developer rebuilding during an
|
||||
# upstream installer outage can opt in, and someone debugging
|
||||
# installer changes on a MITM host can force hard-fail with
|
||||
# `AGENT_VM_BUILD_SOFT_FAIL_AGENTS=0`.
|
||||
#
|
||||
# Default policy: MITM-detected hosts → soft-fail (the same TLS
|
||||
# interception that triggers the CA shim also hits curl 56 on
|
||||
# some GitHub release-asset URLs); clean hosts → hard-fail
|
||||
# (matching production CI).
|
||||
local soft_fail="${AGENT_VM_BUILD_SOFT_FAIL_AGENTS:-${mitm_detected}}"
|
||||
if [ -n "${soft_fail}" ] && [ "${soft_fail}" != "0" ]; then
|
||||
echo "==> Soft-fail mode enabled for agent installers + codestyle clone (AGENT_VM_BUILD_SOFT_FAIL_AGENTS=0 to disable)"
|
||||
extra+=(--build-arg "AGENT_INSTALL_SOFT_FAIL=1")
|
||||
fi
|
||||
|
||||
docker buildx build \
|
||||
-t "${IMAGE_TAG}" \
|
||||
"${extra[@]}" \
|
||||
--output "type=registry,push=true,registry.insecure=true,compression=zstd,compression-level=3,force-compression=true" \
|
||||
-f "${SCRIPT_DIR}/Dockerfile" \
|
||||
"${SCRIPT_DIR}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue