@bongos/core 1.19.1049 → 1.19.1051
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +22 -52
- package/docs/file-map.md +1 -9
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/publish-manifest.js +0 -1
- package/scripts/gds/seed-bv2-tweak-mode-tasks.js +277 -0
- package/scripts/setup-builder.sh +3 -3
- package/src/bongos/module-scope-map.js +2 -15
- package/src/module-api.js +1 -1
- package/.devcontainer/Dockerfile +0 -69
- package/.devcontainer/README.md +0 -73
- package/.devcontainer/devcontainer-lock.json +0 -19
- package/.devcontainer/devcontainer.json +0 -93
- package/.devcontainer/healthcheck.sh +0 -84
- package/.devcontainer/init-firewall.sh +0 -234
- package/.devcontainer/post-create.sh +0 -149
package/.devcontainer/Dockerfile
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# Example Dev Box — the OTB builder environment as code (ADR 0031 §3).
|
|
2
|
-
# Base Ubuntu 24.04 ships Python 3.12 (matches CI's art-pipeline cert) + a non-root
|
|
3
|
-
# `vscode` sudo user. Node 22 / gh / Claude Code arrive as version-pinned features
|
|
4
|
-
# (devcontainer.json); this Dockerfile covers the OS bits features don't: firewall
|
|
5
|
-
# tooling + the art pipeline's system/Python deps. Builds identically on a DO box
|
|
6
|
-
# and a laptop (x64/arm64). Full design + reconciliations: ./README.md.
|
|
7
|
-
|
|
8
|
-
# Pinned to the multi-arch (amd64+arm64) image-index digest, not just the mutable
|
|
9
|
-
# tag, so an upstream tag mutation / supply-chain swap can't silently replace the
|
|
10
|
-
# box foundation. devcontainer-lock.json pins the features; this pins the base.
|
|
11
|
-
# Bump the digest deliberately when intentionally moving to a newer base.
|
|
12
|
-
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04@sha256:d94c97dd9cacf183d0a6fd12a8e87b526e9e928307674ae9c94139139c0c6eae
|
|
13
|
-
|
|
14
|
-
ARG TZ="Etc/UTC"
|
|
15
|
-
ENV TZ="${TZ}"
|
|
16
|
-
ENV DEVCONTAINER=true
|
|
17
|
-
# Persisted shell history (see the volume mount in devcontainer.json).
|
|
18
|
-
ENV HISTFILE=/commandhistory/.bash_history
|
|
19
|
-
|
|
20
|
-
# --- OS packages --------------------------------------------------------------
|
|
21
|
-
# Firewall tooling (iptables/ipset/iproute2 + dnsutils for `dig`), jq for the
|
|
22
|
-
# GitHub-meta parse, the Python 3.12 toolchain, and the sharp from-source net.
|
|
23
|
-
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
24
|
-
&& apt-get install -y --no-install-recommends \
|
|
25
|
-
iptables ipset iproute2 dnsutils \
|
|
26
|
-
jq curl ca-certificates \
|
|
27
|
-
python3 python3-pip python3-venv python3-dev \
|
|
28
|
-
build-essential pkg-config \
|
|
29
|
-
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
30
|
-
|
|
31
|
-
# --- Python venv for the art pipeline (PEP 668 safe) --------------------------
|
|
32
|
-
# Ubuntu 24.04 marks the system Python "externally managed", so a global
|
|
33
|
-
# `pip install` is refused. A dedicated venv on PATH gives the art pipeline a
|
|
34
|
-
# clean, writable 3.12 interpreter where `python3` "just works" — matching the
|
|
35
|
-
# isolated interpreter CI gets from actions/setup-python. Deps themselves are
|
|
36
|
-
# installed from the repo's pinned art/pipeline/requirements.txt at first-run
|
|
37
|
-
# (post-create.sh), so the set stays manifest-driven and reproducible.
|
|
38
|
-
ENV OTB_VENV=/opt/otb-venv
|
|
39
|
-
RUN python3 -m venv "${OTB_VENV}" \
|
|
40
|
-
&& "${OTB_VENV}/bin/pip" install --no-cache-dir --upgrade pip \
|
|
41
|
-
&& chown -R vscode:vscode "${OTB_VENV}"
|
|
42
|
-
# Prepend the venv to PATH for every shell + lifecycle command.
|
|
43
|
-
ENV PATH="${OTB_VENV}/bin:${PATH}"
|
|
44
|
-
|
|
45
|
-
# --- Firewall + health scripts ------------------------------------------------
|
|
46
|
-
# NB the install path: the Claude Code feature ALSO ships its own
|
|
47
|
-
# /usr/local/bin/init-firewall.sh, and features install AFTER this Dockerfile —
|
|
48
|
-
# so installing ours at that path would be silently overwritten by the feature's
|
|
49
|
-
# (which lacks our GDS/Gemini/PyPI allow-list). We install ours as
|
|
50
|
-
# otb-init-firewall.sh and point postStart at it; the feature's copy sits unused.
|
|
51
|
-
COPY init-firewall.sh /usr/local/bin/otb-init-firewall.sh
|
|
52
|
-
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
|
|
53
|
-
RUN chmod +x /usr/local/bin/otb-init-firewall.sh /usr/local/bin/healthcheck.sh
|
|
54
|
-
|
|
55
|
-
# Persisted-history dir owned by the runtime user.
|
|
56
|
-
RUN mkdir -p /commandhistory && chown -R vscode:vscode /commandhistory
|
|
57
|
-
|
|
58
|
-
# --- Build-time integrity assertion -------------------------------------------
|
|
59
|
-
# Fail the BUILD (not some later mystery at runtime) if the toolchain the box
|
|
60
|
-
# promises isn't actually present. This is half of "doctor.js as health check":
|
|
61
|
-
# the image-level half, asserting the box was built right. The builder-level
|
|
62
|
-
# half (session/auth/git) runs as scripts/gds/doctor.js at attach time.
|
|
63
|
-
RUN /usr/local/bin/healthcheck.sh --build || \
|
|
64
|
-
(echo "BUILD FAILED: toolchain integrity check did not pass" && exit 1)
|
|
65
|
-
|
|
66
|
-
# Runtime health check for the DO-box-as-service case (#598): a parked/recreated
|
|
67
|
-
# box can be probed for "is the toolchain intact" without a GDS session.
|
|
68
|
-
HEALTHCHECK --interval=1m --timeout=15s --start-period=30s --retries=3 \
|
|
69
|
-
CMD /usr/local/bin/healthcheck.sh || exit 1
|
package/.devcontainer/README.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# The Example Dev Box (`.devcontainer/`)
|
|
2
|
-
|
|
3
|
-
This folder is **the box defined as code** — [ADR 0031 §3](../docs/adr/0031-cloud-dev-environments-for-builders.md). It is the single environment every OTB builder runs Claude Code's *worker* in, whether they reach it from Claude Desktop → SSH, from the web → Remote Control, or locally via Docker Desktop. The same spec builds and runs identically on a DigitalOcean box and on a laptop.
|
|
4
|
-
|
|
5
|
-
> Recall the mental model ([ADR 0031 "mental model"](../docs/adr/0031-cloud-dev-environments-for-builders.md), and [the onboarding primer](../docs/onboarding/primer.md)): the **screen** is your device, the **brain** is Anthropic's servers, and this is the **worker** — where the repo lives and the work happens.
|
|
6
|
-
|
|
7
|
-
## What's here
|
|
8
|
-
|
|
9
|
-
| File | Role |
|
|
10
|
-
|---|---|
|
|
11
|
-
| `devcontainer.json` | The box spec: base build, version-pinned features (Node 22, GitHub CLI, Claude Code), firewall capabilities, persisted volumes, lifecycle hooks. |
|
|
12
|
-
| `Dockerfile` | Ubuntu 24.04 base + the OS-level bits features don't cover: firewall tooling, the art-pipeline system deps, and a PEP-668-safe Python venv. |
|
|
13
|
-
| `init-firewall.sh` | The **default-deny outbound firewall** (ADR 0031 §6). Runs at every container start. Allow-lists only the hosts the toolchain needs. |
|
|
14
|
-
| `post-create.sh` | First-run provisioning: folds in `setup-builder.sh`, installs art deps, verifies the stack. |
|
|
15
|
-
| `healthcheck.sh` | Image-level toolchain integrity check — the "is the box built right?" half of doctor-as-health-check. |
|
|
16
|
-
|
|
17
|
-
## The toolchain it guarantees
|
|
18
|
-
|
|
19
|
-
- **Node 22** (`package.json` requires `>=22`) — via the `node` devcontainer feature.
|
|
20
|
-
- **Python 3.12** — native to the Ubuntu 24.04 base, installed into a venv at `/opt/otb-venv`.
|
|
21
|
-
- **sharp ^0.34** — the game/art native dep; prebuilt binaries on linux x64 **and** arm64, so no compile.
|
|
22
|
-
- **The five art-pipeline libs** — Pillow, numpy, scipy, scikit-learn, certifi (from the pinned `art/pipeline/requirements.txt`).
|
|
23
|
-
- **Claude Code + GitHub CLI + git** — via features.
|
|
24
|
-
|
|
25
|
-
## Lifecycle
|
|
26
|
-
|
|
27
|
-
1. **build** — Dockerfile lays down the OS deps + venv, then asserts the toolchain (`healthcheck.sh --build`) so a broken box fails the build, not a later mystery.
|
|
28
|
-
2. **postCreate** (once) — `post-create.sh`: brings up the **firewall first** (so the dependency installs below run behind default-deny — no open-egress window during provisioning), then `setup-builder.sh --no-auth --skip-keys` (toolchain check + `npm ci` + `.env.local`), then `pip install` the art deps into the venv, then verify sharp + the art libs load.
|
|
29
|
-
3. **postStart** (every start) — `sudo otb-init-firewall.sh` re-applies the default-deny egress firewall (iptables state resets each container start).
|
|
30
|
-
4. **postAttach** (every attach) — `node scripts/gds/doctor.js` runs the *builder* health check (session/auth/git/env). Non-fatal, so a fresh, not-yet-authed box still attaches.
|
|
31
|
-
|
|
32
|
-
On first connect you finish two runtime steps the box can't do for you: authenticate the GDS CLI (`node scripts/gds/setup.js` or `/builder-setup`) and, if you run the art pipeline, drop a Google AI Studio key in `~/.config/otb/env` (a persisted volume — survives rebuilds).
|
|
33
|
-
|
|
34
|
-
## The firewall allow-list
|
|
35
|
-
|
|
36
|
-
Default-deny outbound. Only these egress targets are permitted (keep it tight — every addition is attack surface):
|
|
37
|
-
|
|
38
|
-
| Target | Why |
|
|
39
|
-
|---|---|
|
|
40
|
-
| Anthropic API + telemetry | the brain (tokens), Claude Code |
|
|
41
|
-
| GitHub (published meta ranges) | `git` push/pull, `gh` |
|
|
42
|
-
| npm registry | `npm ci` |
|
|
43
|
-
| `example.com` + `staging.` | the GDS API (`/api/gds/*` CLI) |
|
|
44
|
-
| `generativelanguage.googleapis.com` | the pixel-art pipeline (Gemini) |
|
|
45
|
-
| `pypi.org` + `files.pythonhosted.org` | `pip install` the art deps |
|
|
46
|
-
|
|
47
|
-
The firewall comes up **before the dependency installs** (in postCreate) and is **re-applied on every start** (postStart), so there is no open-egress window during provisioning — `npm ci` and `pip` run behind the allow-list (npm/PyPI/GitHub are on it). It **fails closed** by default: if it can't apply rules or verification fails, container start fails. The one documented escape hatch is `FIREWALL_OPTIONAL=1`, for a laptop Docker host that won't grant `NET_ADMIN` — there the builder is already root of their own machine, so egress lockdown is advisory (ADR 0031 §6 "honest limit"). Using it emits a loud, greppable `SECURITY AUDIT` line in the container logs. The DO box (#598) leaves it enforcing.
|
|
48
|
-
|
|
49
|
-
## Running it
|
|
50
|
-
|
|
51
|
-
- **Laptop (Docker Desktop):** open the repo in VS Code → "Reopen in Container", or `devcontainer up --workspace-folder .`. Apple-silicon (arm64) and Intel (x64) both work — sharp ships prebuilt binaries for each, and `node_modules` is a container-isolated volume so the host's macOS binaries never collide with the container's Linux ones.
|
|
52
|
-
- **DigitalOcean box:** the per-builder provisioning (#598) builds this same spec on the box and exposes it over SSH / `claude rc`. Nothing in this folder is DO-specific — that's the portability the ADR commits to.
|
|
53
|
-
|
|
54
|
-
### Known macOS-laptop caveat (does not affect the DO box)
|
|
55
|
-
|
|
56
|
-
On a macOS Docker Desktop bind mount, VirtioFS can intermittently return `EDEADLK` ("Resource deadlock avoided") when reading repo files, especially while the host is heavily accessing the same folder. The symptom seen in testing was the postCreate **art-deps `pip install` failing** to read `art/pipeline/requirements.txt` (the box otherwise built fine — Node, Python, `sharp`, the firewall, and the runtime health check all passed). post-create.sh retries and is non-fatal, so the box still provisions. If art deps don't install: rebuild the container, or switch **Docker Desktop → Settings → General → file sharing to "gRPC FUSE"**. The DO box uses a native filesystem (a real clone, no bind mount), so this never occurs there.
|
|
57
|
-
|
|
58
|
-
## Deliberate reconciliations (where this differs from a naïve reading of the ADR)
|
|
59
|
-
|
|
60
|
-
- **Python 3.12, not "3.10".** ADR 0031 §3 says "Python 3.10"; that's a floor. CI (`portability-smoke.yml`) certifies the art pipeline on **3.12**, so the box uses 3.12 to *reproduce* CI rather than approximate it. 3.12 satisfies the `>=3.10` requirement in `setup-builder.sh`.
|
|
61
|
-
- **`sharp` "build deps" without system libvips.** The ADR says "sharp build deps." sharp ^0.34 uses prebuilt binaries with libvips **bundled**, so no compile happens on x64/arm64. We install `build-essential` + `pkg-config` as the from-source safety net the ADR intends, but deliberately do **not** install system `libvips-dev` — a mismatched system libvips is a known footgun; sharp prefers its own.
|
|
62
|
-
- **Allow-list is wider than the ADR's "Anthropic, GitHub, npm".** That parenthetical describes the *Anthropic feature's* defaults. Our box legitimately needs the GDS API + Gemini + PyPI or the CLI and art pipeline break behind the firewall. The list stays minimal and each entry is justified above.
|
|
63
|
-
- **Base = Ubuntu 24.04 devcontainers image** (not Anthropic's reference `node:20`) — for the Node 22 + Python 3.12 parity above and a ready-made non-root `vscode` sudo user. The Claude Code *feature* is still Anthropic's, satisfying "the Anthropic Claude Code container feature."
|
|
64
|
-
|
|
65
|
-
## Out of scope (separate ADR-0031 tasks)
|
|
66
|
-
|
|
67
|
-
This task is the **foundation only**. It does not provision boxes, lock builders to one box, gate by rank, or wire billing:
|
|
68
|
-
|
|
69
|
-
- **#597** — move prod deploy to GitHub Actions (no builder box holds the droplet key).
|
|
70
|
-
- **#598** — per-builder DO box provisioning + auto-suspend (depends on this).
|
|
71
|
-
- **#599** — Desktop managed-settings + the always-on `claude rc` service.
|
|
72
|
-
- **#600** — rank-gated source access via the box.
|
|
73
|
-
- **#602** — cost passthrough (org-funded starter → self-fund).
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"features": {
|
|
3
|
-
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {
|
|
4
|
-
"version": "1.0.5",
|
|
5
|
-
"resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a",
|
|
6
|
-
"integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a"
|
|
7
|
-
},
|
|
8
|
-
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
9
|
-
"version": "1.1.0",
|
|
10
|
-
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
|
|
11
|
-
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
|
|
12
|
-
},
|
|
13
|
-
"ghcr.io/devcontainers/features/node:1": {
|
|
14
|
-
"version": "1.7.1",
|
|
15
|
-
"resolved": "ghcr.io/devcontainers/features/node@sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6",
|
|
16
|
-
"integrity": "sha256:8c0de46939b61958041700ee89e3493f3b2e4131a06dc46b4d9423427d06e5f6"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
// Example Dev Box — the OTB builder environment as code (ADR 0031 §3).
|
|
3
|
-
//
|
|
4
|
-
// This is the single box spec every builder runs, reached via Claude Desktop
|
|
5
|
-
// → SSH or the web → Remote Control (ADR 0031 §1). It builds identically on a
|
|
6
|
-
// DigitalOcean box and on a laptop via Docker Desktop. The pre-distributed
|
|
7
|
-
// Desktop "managed settings" that lock a non-technical builder to exactly this
|
|
8
|
-
// box are a separate task (#599); this file is the box itself.
|
|
9
|
-
"name": "Example Dev Box",
|
|
10
|
-
|
|
11
|
-
"build": {
|
|
12
|
-
"dockerfile": "Dockerfile",
|
|
13
|
-
// Context is this .devcontainer/ folder (the default). The Dockerfile only
|
|
14
|
-
// COPYs the two scripts that live here; nothing from the repo root is needed
|
|
15
|
-
// at build time (npm ci / pip install run at postCreate against the mounted
|
|
16
|
-
// workspace), so the COPY paths stay relative to this folder.
|
|
17
|
-
"context": ".",
|
|
18
|
-
"args": {
|
|
19
|
-
// Honor the host timezone when set, else UTC.
|
|
20
|
-
"TZ": "${localEnv:TZ:Etc/UTC}"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
// Node 22, the GitHub CLI, and Claude Code arrive as version-pinned features
|
|
25
|
-
// rather than hand-rolled in the Dockerfile (the idiomatic, reproducible path).
|
|
26
|
-
// The Claude Code feature also installs Anthropic's default-deny egress posture
|
|
27
|
-
// tooling; our init-firewall.sh (postStart) is the OTB-tuned allow-list on top.
|
|
28
|
-
"features": {
|
|
29
|
-
"ghcr.io/devcontainers/features/node:1": { "version": "22" },
|
|
30
|
-
"ghcr.io/devcontainers/features/github-cli:1": {},
|
|
31
|
-
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
// NET_ADMIN + NET_RAW let the postStart firewall manage iptables/ipset. Without
|
|
35
|
-
// these the firewall fails closed (or warns if FIREWALL_OPTIONAL=1) — see
|
|
36
|
-
// init-firewall.sh. Docker Desktop and a DO Docker host both honor cap-add.
|
|
37
|
-
"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"],
|
|
38
|
-
|
|
39
|
-
// The devcontainers Ubuntu base ships a non-root `vscode` user with passwordless
|
|
40
|
-
// sudo (needed for the postStart firewall). Run as that user, not root.
|
|
41
|
-
"remoteUser": "vscode",
|
|
42
|
-
|
|
43
|
-
"containerEnv": {
|
|
44
|
-
"DEVCONTAINER": "true",
|
|
45
|
-
// Where the art-pipeline Python venv lives (the Dockerfile prepends its bin
|
|
46
|
-
// to PATH); post-create.sh and healthcheck.sh reference it.
|
|
47
|
-
"OTB_VENV": "/opt/otb-venv",
|
|
48
|
-
// A modest Node heap headroom for the dev server + art pipeline.
|
|
49
|
-
"NODE_OPTIONS": "--max-old-space-size=4096"
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
// Standardize the in-container repo path (matches Anthropic's reference) so
|
|
53
|
-
// docs/scripts can assume /workspace regardless of the host folder name.
|
|
54
|
-
"workspaceFolder": "/workspace",
|
|
55
|
-
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
|
56
|
-
|
|
57
|
-
// Named volumes (NOT binds — nothing secret lands in the repo or image, per
|
|
58
|
-
// ADR 0022):
|
|
59
|
-
// - node_modules: a container-ISOLATED volume so the host's native binaries
|
|
60
|
-
// (e.g. a macOS-built `sharp`) never leak into this linux box, and vice
|
|
61
|
-
// versa. This is what makes "runs identically on a laptop" true — without
|
|
62
|
-
// it, bind-mounting the repo from macOS breaks sharp in the container.
|
|
63
|
-
// - the Claude Code login/config (so a rebuild doesn't force re-login),
|
|
64
|
-
// - shell history (quality-of-life),
|
|
65
|
-
// - ~/.config/otb (the per-builder Gemini key + GDS session survive rebuilds).
|
|
66
|
-
"mounts": [
|
|
67
|
-
"source=otb-node-modules-${devcontainerId},target=/workspace/node_modules,type=volume",
|
|
68
|
-
"source=otb-claude-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
|
|
69
|
-
"source=otb-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
|
|
70
|
-
"source=otb-config-${devcontainerId},target=/home/vscode/.config/otb,type=volume"
|
|
71
|
-
],
|
|
72
|
-
|
|
73
|
-
// Lifecycle:
|
|
74
|
-
// postCreate — once, after the repo is mounted: fold in setup-builder.sh,
|
|
75
|
-
// install art deps, verify the stack (post-create.sh).
|
|
76
|
-
// postStart — every start: bring up the default-deny firewall.
|
|
77
|
-
// postAttach — every attach: run the BUILDER health check (session/auth/git).
|
|
78
|
-
// Non-fatal (|| true) so a not-yet-authed fresh box still attaches.
|
|
79
|
-
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
|
80
|
-
"postStartCommand": "sudo /usr/local/bin/otb-init-firewall.sh",
|
|
81
|
-
"postAttachCommand": "node scripts/gds/doctor.js || true",
|
|
82
|
-
"waitFor": "postCreateCommand",
|
|
83
|
-
|
|
84
|
-
"customizations": {
|
|
85
|
-
"vscode": {
|
|
86
|
-
"extensions": [
|
|
87
|
-
"anthropic.claude-code",
|
|
88
|
-
"dbaeumer.vscode-eslint",
|
|
89
|
-
"esbenp.prettier-vscode"
|
|
90
|
-
]
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# healthcheck.sh — image-level toolchain integrity check for the Example
|
|
3
|
-
# Dev Box (ADR 0031 §3).
|
|
4
|
-
#
|
|
5
|
-
# This is the IMAGE half of "doctor.js as the health check": it answers "was the
|
|
6
|
-
# box built correctly?" using only what the image itself provides — no GDS
|
|
7
|
-
# session, no repo checkout, no network. The BUILDER half ("is *this builder*
|
|
8
|
-
# authed, hooked, and clean?") is scripts/gds/doctor.js, run at attach time.
|
|
9
|
-
#
|
|
10
|
-
# Two modes:
|
|
11
|
-
# --build run as the final Dockerfile RUN step (fail the build on a bad box).
|
|
12
|
-
# (default) the runtime HEALTHCHECK / a recreated-box probe (#598).
|
|
13
|
-
#
|
|
14
|
-
# Both check the same stable, image-level toolchain. Repo-level facts (sharp in
|
|
15
|
-
# node_modules, the art libs in the venv) are installed at first-run by
|
|
16
|
-
# post-create.sh and verified there — they are intentionally NOT gated here so
|
|
17
|
-
# the container never flaps "unhealthy" during provisioning.
|
|
18
|
-
#
|
|
19
|
-
# NOTE on ordering: Node, the GitHub CLI, and Claude Code are installed by
|
|
20
|
-
# devcontainer *features*, which run AFTER the Dockerfile is built. So the
|
|
21
|
-
# --build assertion can't see Node — it checks only what the Dockerfile itself
|
|
22
|
-
# provides (the Python venv + firewall tooling). The Node check runs at runtime,
|
|
23
|
-
# where features are present.
|
|
24
|
-
|
|
25
|
-
set -uo pipefail
|
|
26
|
-
|
|
27
|
-
MODE="${1:-runtime}"
|
|
28
|
-
fails=0
|
|
29
|
-
|
|
30
|
-
ok() { echo " ok $*"; }
|
|
31
|
-
bad() { echo " FAIL $*"; fails=$((fails + 1)); }
|
|
32
|
-
|
|
33
|
-
# Node >= 22 — runtime only (feature-installed after the Dockerfile build).
|
|
34
|
-
if [ "$MODE" != "--build" ]; then
|
|
35
|
-
if command -v node >/dev/null 2>&1; then
|
|
36
|
-
nver="$(node --version 2>/dev/null)"
|
|
37
|
-
nmaj="$(echo "$nver" | sed 's/^v//' | cut -d. -f1)"
|
|
38
|
-
if [[ "$nmaj" =~ ^[0-9]+$ ]] && [ "$nmaj" -ge 22 ]; then
|
|
39
|
-
ok "node $nver (>= 22)"
|
|
40
|
-
else
|
|
41
|
-
bad "node $nver found, need >= 22"
|
|
42
|
-
fi
|
|
43
|
-
else
|
|
44
|
-
bad "node not on PATH"
|
|
45
|
-
fi
|
|
46
|
-
else
|
|
47
|
-
echo " -- node check deferred to runtime (feature-installed after the Dockerfile)"
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Python >= 3.12 (the venv interpreter — must be first on PATH)
|
|
51
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
52
|
-
pyver="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])' 2>/dev/null || echo "?")"
|
|
53
|
-
pymaj="${pyver%%.*}"; pymin="${pyver#*.}"
|
|
54
|
-
if [[ "$pymaj" =~ ^[0-9]+$ ]] && [[ "$pymin" =~ ^[0-9]+$ ]] \
|
|
55
|
-
&& { [ "$pymaj" -gt 3 ] || { [ "$pymaj" -eq 3 ] && [ "$pymin" -ge 12 ]; }; }; then
|
|
56
|
-
ok "python ${pyver} (>= 3.12) [$(command -v python3)]"
|
|
57
|
-
else
|
|
58
|
-
bad "python ${pyver} found, need >= 3.12"
|
|
59
|
-
fi
|
|
60
|
-
# The venv must be the active interpreter so the art pipeline's deps resolve.
|
|
61
|
-
if [ -n "${OTB_VENV:-}" ] && [ "$(command -v python3)" = "${OTB_VENV}/bin/python3" ]; then
|
|
62
|
-
ok "art-pipeline venv active (${OTB_VENV})"
|
|
63
|
-
else
|
|
64
|
-
bad "expected venv python at ${OTB_VENV:-<unset>}/bin/python3, got $(command -v python3)"
|
|
65
|
-
fi
|
|
66
|
-
else
|
|
67
|
-
bad "python3 not on PATH"
|
|
68
|
-
fi
|
|
69
|
-
|
|
70
|
-
# Firewall tooling present (the default-deny boundary depends on it).
|
|
71
|
-
for tool in iptables ipset dig jq; do
|
|
72
|
-
if command -v "$tool" >/dev/null 2>&1; then
|
|
73
|
-
ok "$tool present"
|
|
74
|
-
else
|
|
75
|
-
bad "$tool missing (firewall/init would fail)"
|
|
76
|
-
fi
|
|
77
|
-
done
|
|
78
|
-
|
|
79
|
-
if [ "$fails" -eq 0 ]; then
|
|
80
|
-
[ "$MODE" = "--build" ] && echo "toolchain integrity: OK (build)"
|
|
81
|
-
exit 0
|
|
82
|
-
fi
|
|
83
|
-
echo "toolchain integrity: ${fails} problem(s)"
|
|
84
|
-
exit 1
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# init-firewall.sh — default-deny outbound firewall for the Example Dev Box.
|
|
3
|
-
#
|
|
4
|
-
# ADR 0031 §3 + §6: the box is an org-controlled, egress-restricted surface. This
|
|
5
|
-
# script flips the container to default-DROP and then allow-lists ONLY the hosts
|
|
6
|
-
# the OTB toolchain legitimately needs. Everything else is blocked, so a builder
|
|
7
|
-
# session (or a compromised dependency) cannot quietly exfiltrate to an arbitrary
|
|
8
|
-
# host.
|
|
9
|
-
#
|
|
10
|
-
# Mechanism (mirrors Anthropic's reference init-firewall.sh):
|
|
11
|
-
# 1. Resolve every allow-listed host to IPs WHILE egress is still open.
|
|
12
|
-
# 2. Stuff them into an `ipset` (hash:net so GitHub's CIDR ranges fit).
|
|
13
|
-
# 3. Flip INPUT/OUTPUT/FORWARD to DROP, then re-allow loopback, established
|
|
14
|
-
# connections, DNS, SSH, and OUTPUT to the ipset.
|
|
15
|
-
# 4. Verify: a blocked host must fail; an allow-listed host must succeed.
|
|
16
|
-
#
|
|
17
|
-
# Allow-list (and WHY each is here — keep this tight, every addition is egress):
|
|
18
|
-
# - GitHub (api.github.com/meta ranges) ......... git push/pull, `gh`
|
|
19
|
-
# - npm registry (registry.npmjs.org) ............ `npm ci`
|
|
20
|
-
# - Anthropic API (api.anthropic.com + telemetry) the brain (tokens), Claude Code
|
|
21
|
-
# - THIS instance's own API host(s) .............. the GDS API (/api/gds/* CLI)
|
|
22
|
-
# (resolved from config/branding.json at runtime — task 1002757; a hardcoded
|
|
23
|
-
# founder host allow-listed someone else's server and BLOCKED the builder's own)
|
|
24
|
-
# - generativelanguage.googleapis.com ............ the pixel-art pipeline (Gemini) [OTB]
|
|
25
|
-
# - pypi.org + files.pythonhosted.org ............ `pip install` art deps [OTB]
|
|
26
|
-
#
|
|
27
|
-
# Runs as root via `sudo` from the devcontainer postStartCommand. Needs the
|
|
28
|
-
# NET_ADMIN + NET_RAW capabilities (granted by runArgs in devcontainer.json).
|
|
29
|
-
#
|
|
30
|
-
# Fail posture: FAIL-CLOSED by default. If the rules cannot be applied or the
|
|
31
|
-
# verification fails, the script exits non-zero. Set FIREWALL_OPTIONAL=1 to
|
|
32
|
-
# downgrade a cap/permission failure to a loud warning that still lets the
|
|
33
|
-
# container boot — intended ONLY for a laptop Docker host that won't grant
|
|
34
|
-
# NET_ADMIN (where the builder is already root of their own machine, so egress
|
|
35
|
-
# lockdown is advisory anyway — see ADR 0031 §6 "honest limit"). The DO box
|
|
36
|
-
# (#598) sets FIREWALL_OPTIONAL unset, so it always enforces.
|
|
37
|
-
|
|
38
|
-
set -euo pipefail
|
|
39
|
-
IFS=$'\n\t'
|
|
40
|
-
|
|
41
|
-
OPTIONAL="${FIREWALL_OPTIONAL:-0}"
|
|
42
|
-
|
|
43
|
-
# Hosts to allow-list by name (resolved via dig at runtime). GitHub is handled
|
|
44
|
-
# separately via its published meta ranges. Keep this list minimal.
|
|
45
|
-
ALLOWED_HOSTS=(
|
|
46
|
-
"registry.npmjs.org"
|
|
47
|
-
"api.anthropic.com"
|
|
48
|
-
"statsig.anthropic.com"
|
|
49
|
-
"sentry.io"
|
|
50
|
-
"generativelanguage.googleapis.com"
|
|
51
|
-
"pypi.org"
|
|
52
|
-
"files.pythonhosted.org"
|
|
53
|
-
# THIS instance's own hosts are appended below from its branding pack.
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Append the instance's OWN API hosts, read from config/branding.json at runtime
|
|
57
|
-
# (task 1002757). Was two hardcoded founder hosts, which allow-listed someone
|
|
58
|
-
# else's server and left the builder's own GDS API blocked — the devcontainer
|
|
59
|
-
# then failed every /builder-* call for a reason nothing in the log explained.
|
|
60
|
-
# GDS_ALLOW_HOSTS (space/comma separated) overrides for a non-standard setup.
|
|
61
|
-
REPO_ROOT_FW="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
62
|
-
INSTANCE_HOSTS="${GDS_ALLOW_HOSTS:-}"
|
|
63
|
-
if [ -z "$INSTANCE_HOSTS" ]; then
|
|
64
|
-
INSTANCE_HOSTS="$(node -e '
|
|
65
|
-
const d = (require(process.argv[1]).branding().domains) || {};
|
|
66
|
-
const hosts = new Set();
|
|
67
|
-
// Only *_ORIGIN-shaped values yield a host, and only a real public FQDN
|
|
68
|
-
// counts: the neutral starter pack carries localhost:3000 and bare subdomain
|
|
69
|
-
// PREFIXES ("sandbox-", "term-"), none of which is an allow-listable host.
|
|
70
|
-
for (const v of Object.values(d)) {
|
|
71
|
-
if (typeof v !== "string" || !v.trim()) continue;
|
|
72
|
-
let host = null;
|
|
73
|
-
try { host = new URL(v).hostname; } catch { host = v.trim(); }
|
|
74
|
-
if (!host) continue;
|
|
75
|
-
if (!/^(?=.{1,253}$)[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i.test(host)) continue;
|
|
76
|
-
if (/^localhost$/i.test(host)) continue;
|
|
77
|
-
hosts.add(host.toLowerCase());
|
|
78
|
-
}
|
|
79
|
-
process.stdout.write([...hosts].join(" "));
|
|
80
|
-
' "$REPO_ROOT_FW/src/branding" 2>/dev/null || true)"
|
|
81
|
-
fi
|
|
82
|
-
for h in ${INSTANCE_HOSTS//,/ }; do
|
|
83
|
-
[ -n "$h" ] && ALLOWED_HOSTS+=("$h")
|
|
84
|
-
done
|
|
85
|
-
|
|
86
|
-
log() { echo "[firewall] $*"; }
|
|
87
|
-
die() {
|
|
88
|
-
# Honor the documented escape hatch: a cap/permission failure on a laptop
|
|
89
|
-
# host downgrades to a warning instead of bricking container startup.
|
|
90
|
-
# The bypass is env-driven, so it can't be access-controlled from inside the
|
|
91
|
-
# script — the mitigation is a LOUD, greppable audit line so any use is
|
|
92
|
-
# visible in container logs / log aggregation. On the org DO box this must
|
|
93
|
-
# never be set; if this banner appears there, investigate.
|
|
94
|
-
if [ "$OPTIONAL" = "1" ]; then
|
|
95
|
-
log "==================== SECURITY AUDIT ===================="
|
|
96
|
-
log "egress firewall DISABLED via FIREWALL_OPTIONAL=1"
|
|
97
|
-
log " reason: $*"
|
|
98
|
-
log " the box is running WITHOUT default-deny egress (laptop-only escape hatch)"
|
|
99
|
-
log "======================================================="
|
|
100
|
-
exit 0
|
|
101
|
-
fi
|
|
102
|
-
log "ERROR: $*"
|
|
103
|
-
log "If this host genuinely cannot grant NET_ADMIN (e.g. a restricted laptop Docker),"
|
|
104
|
-
log "re-run with FIREWALL_OPTIONAL=1 to boot without egress lockdown."
|
|
105
|
-
exit 1
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
# Pre-flight: we must be root and have the iptables/ipset tooling + caps.
|
|
109
|
-
if [ "$(id -u)" -ne 0 ]; then
|
|
110
|
-
die "must run as root (expected: sudo $0)"
|
|
111
|
-
fi
|
|
112
|
-
command -v iptables >/dev/null 2>&1 || die "iptables not installed"
|
|
113
|
-
command -v ipset >/dev/null 2>&1 || die "ipset not installed"
|
|
114
|
-
command -v dig >/dev/null 2>&1 || die "dig (dnsutils) not installed"
|
|
115
|
-
|
|
116
|
-
# A NET_ADMIN probe: try a harmless list. If the kernel refuses, we lack the cap.
|
|
117
|
-
if ! iptables -L >/dev/null 2>&1; then
|
|
118
|
-
die "cannot manage iptables (NET_ADMIN capability missing?)"
|
|
119
|
-
fi
|
|
120
|
-
|
|
121
|
-
log "resolving allow-list while egress is still open…"
|
|
122
|
-
|
|
123
|
-
# Fresh ipset. hash:net so we can store both single IPs and CIDR ranges.
|
|
124
|
-
# create -exist + flush (not destroy+create): on a manual re-run an iptables
|
|
125
|
-
# OUTPUT rule may still reference the set, which makes `destroy` fail. Flushing
|
|
126
|
-
# empties it in place while keeping the object valid.
|
|
127
|
-
ipset create allowed-domains hash:net -exist
|
|
128
|
-
ipset flush allowed-domains
|
|
129
|
-
|
|
130
|
-
add_ip() {
|
|
131
|
-
# Validate it looks like an IPv4 (or CIDR) before adding, so a bad DNS answer
|
|
132
|
-
# can't inject garbage into the set.
|
|
133
|
-
local ip="$1"
|
|
134
|
-
if [[ "$ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}(/[0-9]{1,2})?$ ]]; then
|
|
135
|
-
ipset add allowed-domains "$ip" 2>/dev/null || true
|
|
136
|
-
fi
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
# --- GitHub: use the published meta ranges (covers github.com, api, git, web) -
|
|
140
|
-
log " + GitHub meta ranges"
|
|
141
|
-
gh_meta="$(curl -fsS --max-time 20 https://api.github.com/meta || true)"
|
|
142
|
-
if [ -n "$gh_meta" ] && command -v jq >/dev/null 2>&1; then
|
|
143
|
-
# .web + .api + .git are the egress-relevant range groups.
|
|
144
|
-
while read -r cidr; do
|
|
145
|
-
[ -n "$cidr" ] && add_ip "$cidr"
|
|
146
|
-
done < <(echo "$gh_meta" | jq -r '(.web // []) + (.api // []) + (.git // []) | .[]' 2>/dev/null | grep -E '^[0-9]+\.' || true)
|
|
147
|
-
else
|
|
148
|
-
# Fallback: resolve the hostnames directly if the meta API is unavailable.
|
|
149
|
-
log " (meta API unavailable — falling back to direct resolution)"
|
|
150
|
-
for h in github.com api.github.com codeload.github.com objects.githubusercontent.com; do
|
|
151
|
-
while read -r ip; do add_ip "$ip"; done < <(dig +short +noall +answer A "$h" 2>/dev/null | grep -E '^[0-9]+\.' || true)
|
|
152
|
-
done
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
# --- Everything else: resolve by name -----------------------------------------
|
|
156
|
-
for host in "${ALLOWED_HOSTS[@]}"; do
|
|
157
|
-
log " + $host"
|
|
158
|
-
while read -r ip; do
|
|
159
|
-
add_ip "$ip"
|
|
160
|
-
done < <(dig +short +noall +answer A "$host" 2>/dev/null | grep -E '^[0-9]+\.' || true)
|
|
161
|
-
done
|
|
162
|
-
|
|
163
|
-
# --- Host networking: keep the container reachable from / to its Docker host --
|
|
164
|
-
# Allow the host gateway + the container's own /24 so SSH in, the dev server,
|
|
165
|
-
# and DNS-to-the-Docker-resolver keep working after we flip to DROP.
|
|
166
|
-
host_ip="$(ip route | awk '/default/ {print $3; exit}')"
|
|
167
|
-
if [ -n "${host_ip:-}" ]; then
|
|
168
|
-
host_net="$(echo "$host_ip" | sed 's/\.[0-9]*$/.0\/24/')"
|
|
169
|
-
log " + host network ${host_net}"
|
|
170
|
-
add_ip "$host_net"
|
|
171
|
-
fi
|
|
172
|
-
|
|
173
|
-
n="$(ipset list allowed-domains | grep -cE '^[0-9]+\.' || true)"
|
|
174
|
-
log "allow-list built: ${n} entries"
|
|
175
|
-
[ "$n" -gt 0 ] || die "allow-list is empty — refusing to flip to default-DROP (would sever everything)"
|
|
176
|
-
|
|
177
|
-
log "applying default-deny policy…"
|
|
178
|
-
|
|
179
|
-
# Flush prior rules so re-runs are idempotent.
|
|
180
|
-
iptables -F
|
|
181
|
-
iptables -X 2>/dev/null || true
|
|
182
|
-
|
|
183
|
-
# Loopback — always.
|
|
184
|
-
iptables -A INPUT -i lo -j ACCEPT
|
|
185
|
-
iptables -A OUTPUT -o lo -j ACCEPT
|
|
186
|
-
|
|
187
|
-
# Established/related return traffic.
|
|
188
|
-
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
189
|
-
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
190
|
-
|
|
191
|
-
# DNS — keep resolution working at runtime (long-running procs re-resolve).
|
|
192
|
-
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
|
193
|
-
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
|
194
|
-
|
|
195
|
-
# SSH — the Desktop/Remote-Control on-ramp (ADR 0031 §1).
|
|
196
|
-
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
|
197
|
-
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
|
|
198
|
-
|
|
199
|
-
# The actual allow-list: outbound only to resolved, vetted IPs.
|
|
200
|
-
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
|
|
201
|
-
|
|
202
|
-
# Default DROP for everything not explicitly allowed above.
|
|
203
|
-
iptables -P INPUT DROP
|
|
204
|
-
iptables -P FORWARD DROP
|
|
205
|
-
iptables -P OUTPUT DROP
|
|
206
|
-
|
|
207
|
-
log "verifying…"
|
|
208
|
-
|
|
209
|
-
# A blocked host MUST fail (proves the deny is real).
|
|
210
|
-
if curl -fsS --max-time 6 https://example.com >/dev/null 2>&1; then
|
|
211
|
-
die "verification FAILED: example.com is reachable but should be blocked"
|
|
212
|
-
fi
|
|
213
|
-
log " ✓ blocked host (example.com) is unreachable"
|
|
214
|
-
|
|
215
|
-
# An allow-listed host MUST succeed (proves we didn't sever everything).
|
|
216
|
-
if ! curl -fsS --max-time 10 https://api.github.com/zen >/dev/null 2>&1; then
|
|
217
|
-
die "verification FAILED: api.github.com is unreachable but should be allowed"
|
|
218
|
-
fi
|
|
219
|
-
log " ✓ allow-listed host (api.github.com) is reachable"
|
|
220
|
-
|
|
221
|
-
# Our own GDS API must be reachable (the CLI depends on it). Non-fatal warn:
|
|
222
|
-
# the host may be mid-deploy; the firewall correctness is already proven above.
|
|
223
|
-
# The instance's OWN first API host (task 1002757 — was a founder literal, so this
|
|
224
|
-
# probe reported a stranger's health as the container's connectivity).
|
|
225
|
-
GDS_PROBE_HOST="$(printf '%s\n' ${INSTANCE_HOSTS//,/ } | head -n 1)"
|
|
226
|
-
if [ -z "$GDS_PROBE_HOST" ]; then
|
|
227
|
-
log " · no instance API host resolved from the branding pack — skipping the GDS reachability probe"
|
|
228
|
-
elif curl -fsS --max-time 10 "https://$GDS_PROBE_HOST/healthz" 2>/dev/null | grep -q '^ok$'; then
|
|
229
|
-
log " ✓ GDS API ($GDS_PROBE_HOST) is reachable"
|
|
230
|
-
else
|
|
231
|
-
log " ! GDS API healthz did not return ok (host may be redeploying) — allow rule is in place"
|
|
232
|
-
fi
|
|
233
|
-
|
|
234
|
-
log "default-deny firewall active."
|