feat: air-gapped install - #114
Open
dawidaksamski wants to merge 13 commits into
Open
Conversation
dawidaksamski
marked this pull request as ready for review
September 2, 2026 11:05
dawidaksamski
requested review from
librowski,
lukasz-jazwa,
piotrblaszczyk and
szymon-t-sc
as code owners
September 2, 2026 11:05
dawidaksamski
force-pushed
the
feat/WB-596-connection-config
branch
from
September 3, 2026 11:01
d348521 to
cf4f5de
Compare
dawidaksamski
force-pushed
the
feat/WB-596-air-gapped-install
branch
from
September 3, 2026 12:02
8c7f3b7 to
1381f71
Compare
dawidaksamski
force-pushed
the
feat/WB-596-connection-config
branch
from
September 4, 2026 11:50
cf4f5de to
168c5d9
Compare
dawidaksamski
force-pushed
the
feat/WB-596-air-gapped-install
branch
3 times, most recently
from
September 8, 2026 14:42
a5727ba to
d6c149d
Compare
`pnpm install --offline` only stops pnpm's own resolver; lifecycle scripts and build commands still ran with network access, so a script could download outside pnpm and the ordinary build would pass. Every RUN after `pnpm fetch` now carries --network=none, which makes BuildKit block egress for the whole step. Only base-image pulls, the pnpm bootstrap and `pnpm fetch` keep access. Add tools/check-offline-build.mjs, which fails when a post-fetch RUN loses the flag, and run it from `pnpm check`, lint-staged and pr-check.yml. Both images build under the new flags; a whole-build `docker build --network none` is not the test because bootstrap and fetch need the registry by design.
…inor tag The air-gap procedure was four hand-typed commands and called the infra images pinned while postgres:16 floated across every 16.x release. Pin it to 16.15 in both compose files and add pack-offline.sh, which builds, pulls, saves one tarball and writes its sha256 plus a docker image inspect manifest, so a shipped bundle is verifiable and traceable to the exact images it holds. Tags stay tags: digest pins would freeze base-image security patches with no Renovate to bump them, and docker save/load drops digest references, so digest-pinned compose fields could never resolve against loaded images.
The air-gap section described the image bundle without saying it is the one supported model. Record the scope decision: prebuilt-image transport only, no registry mirror procedure and no .npmrc. Explain why the bundle is platform-specific (base images plus the esbuild and swc native packages pnpm selects at install; the Temporal core is not a constraint, one package carries every platform's binary) and make "build for the destination platform" a rule with the manifest's platform column as the check.
…ress conditions The README called an internal AI_BASE_URL "zero egress" while the deployed SPA still fetched Poppins from fonts.googleapis.com on every load, and the worker had a fixed Tavily endpoint behind TAVILY_API_KEY. Remove the CDN links: the SDK stylesheet already inlines Poppins 300-700, and a test now guards index.html against external references. Document the three runtime egress paths (LLM endpoint, Tavily, Turnstile) with the setting that closes each.
…d context pack-offline.sh defaulted to a directory inside deploy/ai-studio, and the README pointed at the repo root, which is the build context of both images: the next `docker compose build` would copy a 1 GB tarball into the runtime image, and git would show the bundle as untracked. Resolve the output path before the cd, default to ~/ai-studio-offline, refuse any path under the checkout, and ignore the directory and *.tar in .gitignore and .dockerignore as a second line.
`# syntax=docker/dockerfile:1` made every build download and run the Dockerfile parser from a floating tag: a fourth network touch the README did not count, and the one build input with no pin at all. Docker Engine 23+ ships a built-in frontend that supports RUN --mount and RUN --network, so drop the directive, have check-offline-build.mjs reject it if it comes back, and document the Docker version the packing machine needs.
The runtime stage inherits pnpm fetch's whole-lockfile virtual store. Record the limitation and the pnpm deploy direction at the stage instead of leaving it to be rediscovered.
…from .env check-offline-build.mjs accepted --network=none anywhere in the RUN text, never checked the pnpm fetch step itself, and treated an escaped trailing backslash as a continuation, so a chained command, a flag inside the shell command, or a hidden RUN all passed. Check only the instruction's own flags, require the boundary step to be exactly `pnpm fetch`, and count trailing backslashes. pack-offline.sh ran compose with the developer's ./.env: COMPOSE_FILE could drop Temporal from the bundle while the shipped override still expected it, and RUNTIME_IMAGE renamed what got saved. Build from .env.example defaults via --env-file /dev/null, and pull base images so a stale local tag never ships.
…ild context Fix drift the review found: the "What runs" table still said postgres:16, nginx was called an exact pin, the Turnstile row implied an operator choice that does not exist, Poppins was described as inline CSS while the SPA serves font files, and the pnpm bootstrap comment called itself the only step needing the registry. Shorten the Dockerfile comments to the three-line ceiling, isolate the first sed step too, and drop the ticket IDs from the README and compose headers. CLAUDE.md claimed knip runs in `pnpm check`; it never did. .dockerignore now excludes npm/yarn auth files and key material, matches compose files at any depth, and loses a negation for a directory that does not exist.
…ed LLM key OPENROUTER_API_KEY was renamed to AI_API_KEY and AI_BASE_URL became required, but a stale apps/backend/.env or apps/execution-worker/.env fails silently: the apps boot with AI off and AI nodes report ai_not_configured. Preflight now warns on the retired name, or on AI_MODEL without AI_API_KEY and AI_BASE_URL, reading key names only. CLAUDE.md tells Claude to run preflight after a pull and offer the migration without touching the key unasked.
…ack-offline from the shell env
dawidaksamski
force-pushed
the
feat/WB-596-air-gapped-install
branch
from
September 8, 2026 15:23
d6c149d to
c931bfb
Compare
dawidaksamski
removed this pull request from stack #115
September 9, 2026 15:33
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
RUNafterpnpm fetchis--network=none, so BuildKit cuts egress for the wholestep — installs, lifecycle scripts and build commands alike (
--offlineon pnpm alone only stopped pnpm's resolver). Only pulling base images,
installing pnpm itself, and
pnpm fetchtouch the registry.tools/check-offline-build.mjsguards that boundary: it fails on apost-fetch
RUNwithout the flag, anything chained onto the fetch step,any other
--network=value, and a# syntax=directive. Wired intopnpm check, lint-staged and PR Check.# syntax=line: it downloaded the BuildKit frontend from Docker Hubby floating tag, an unpinned fourth network touch. Docker Engine 23+
provides the frontend built in.
useNodeVersionis stripped inside the image — pnpm would otherwisedownload its own Node from nodejs.org, pointless on the pinned base image
and fatal without network. Local dev keeps managed Node.
deploy/ai-studio/pack-offline.shproduces the bundle:build --pull,pull infra images, save one tarball, write its sha256 and a
docker image inspectmanifest, copy the compose files and.env.example. It ignores the packer's local.envand refuses an outputdirectory inside the checkout (the repo root is the build context).
16.15). Digests weredeliberately not used: they freeze base-image security patches with no
Renovate to bump them, and
docker save/loaddrops digest references socompose could not resolve them on the host. The manifest records the
digests that shipped.
apps/icons: pinned@svgr/clidevDependency replaces thenpxshell-out; lockfile diff confined to that graph.
(the SDK stylesheet bundles it; a test guards
index.html), and the READMEnames the three optional runtime paths with the setting that closes each —
AI_BASE_URL,TAVILY_API_KEY, Turnstile (always closed here).the checksum,
docker load,up -d --no-build, how to connect. States thesupported model (prebuilt images only; no registry mirror or
.npmrc,superseding WB-601's original path) and the platform rule (build for the
destination; esbuild/swc are per-platform, the Temporal core bundles all).
pnpm fetch(pre-existing, ~1.5 GB) — tracked as WB-631, marked in the Dockerfile.
Verification
docker compose buildof both targets with no env vars set; the log showszero frontend image resolutions. Egress under
--network=noneconfirmedblocked (DNS fails) on the built-in frontend.
pnpm fetchserved from layer cache andthe
pnpm-storemount pruned, both--offlineinstalls still pass — thefetch layer materialises the virtual store.
fetch, escaped trailing backslash, duplicate
--network,# syntax=) andpasses on the real file.
pack-offline.shend to end with a decoy.envpresent: five-imagetarball, checksum verifies. Host simulation: infra images removed, tarball
loaded,
docker compose up --no-build --dry-runcreates every containerwithout a pull.
pass; lockfile regenerates to a fixpoint and
--frozen-lockfileaccepts it./api/healthok, SPA 200, workerRUNNINGwith zero restarts.After deploy
Instruction on what to update once w decide to deploy the AI Studio:
https://claude.ai/code/artifact/8aa970be-58fc-4e92-8bbc-d30ee07c2cbb?via=auto_preview