@bongos/core 1.19.1049 → 1.19.1050

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.1049",
6
- "core_contract": "1.19.1049",
7
- "source_commit": "9d25b951a2f85a4303027917ebf5c95d02cac06e",
5
+ "core_version": "1.19.1050",
6
+ "core_contract": "1.19.1050",
7
+ "source_commit": "58c76064d5864e4579213b609e0113be3685e0ac",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-27T13:43:34.143Z",
9
+ "built_at": "2026-09-27T13:58:16.724Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 543,
12
+ "docs_redacted": 542,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2513,
14
+ "functional_verbatim": 2507,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3083,
20
- "tree_sha256": "a84ed2035d842841f5e0b994df4754f714dc15c3ff0f080cef771d939cd37e7b",
19
+ "file_count": 3076,
20
+ "tree_sha256": "d89a3236ad7eedff9ab8575517a3ff1e564ae1bbac3b72a37cace58ee3ac8f0a",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -259,41 +259,6 @@
259
259
  "mode": "0000644",
260
260
  "sha256": "6548fe1de89c6909def02b76f022cae739c5b129d179a63538b9a1ab252e5030"
261
261
  },
262
- {
263
- "path": ".devcontainer/Dockerfile",
264
- "mode": "0000644",
265
- "sha256": "8e2a619a3d72bfcb91440fd5773622fefb773b5effea1e6c7f7d6c2e01f14817"
266
- },
267
- {
268
- "path": ".devcontainer/README.md",
269
- "mode": "0000644",
270
- "sha256": "33038ae214c708ce3e5c2539c5e672e82da76b882df93d6d906a041fc22253bb"
271
- },
272
- {
273
- "path": ".devcontainer/devcontainer-lock.json",
274
- "mode": "0000644",
275
- "sha256": "ef6b1d2b6d7fdaea6d1a6b0d91378fa18e2f9f49bdf8ffbf541f8659d68574dc"
276
- },
277
- {
278
- "path": ".devcontainer/devcontainer.json",
279
- "mode": "0000644",
280
- "sha256": "4ea21a7f615b5f9cb3a657f6c053d3680aed99c9c33cc70cbac398068fd6c309"
281
- },
282
- {
283
- "path": ".devcontainer/healthcheck.sh",
284
- "mode": "0000644",
285
- "sha256": "5b04ae8e3436e659339eb0f820623844d22ccdbd07d6e9775aa6df17f6d1dd30"
286
- },
287
- {
288
- "path": ".devcontainer/init-firewall.sh",
289
- "mode": "0000644",
290
- "sha256": "bcff88c5ca2835456f43992b6d9506e62ffb1475e20d7b15819f4572f5abb5ee"
291
- },
292
- {
293
- "path": ".devcontainer/post-create.sh",
294
- "mode": "0000644",
295
- "sha256": "ba1f564c1d91473e545a1e33051a4c1952db037e5e607b1d219bde97280174b7"
296
- },
297
262
  {
298
263
  "path": ".dockerignore",
299
264
  "mode": "0000644",
@@ -2762,7 +2727,7 @@
2762
2727
  {
2763
2728
  "path": "docs/file-map.md",
2764
2729
  "mode": "0000644",
2765
- "sha256": "a964665831275c3ec74a9350e758958f05ad61b5a7de569c1291565e1cd84366"
2730
+ "sha256": "64d65a849596466c7fbafba381668129c2e305e88af805dc5eed06a5b06600bb"
2766
2731
  },
2767
2732
  {
2768
2733
  "path": "docs/handoff-template.md",
@@ -2772,7 +2737,7 @@
2772
2737
  {
2773
2738
  "path": "docs/module-api-changelog.md",
2774
2739
  "mode": "0000644",
2775
- "sha256": "113ce655a21853395a81cfdcf467e9de8e1e8b46e5dfe15b1dd5f4d9401e0193"
2740
+ "sha256": "cdd113e011377d05ef9d923798dea05b1c6ee4936ca4e026fd402395ed0b1223"
2776
2741
  },
2777
2742
  {
2778
2743
  "path": "docs/modules-contract.md",
@@ -8497,12 +8462,12 @@
8497
8462
  {
8498
8463
  "path": "package-lock.json",
8499
8464
  "mode": "0000644",
8500
- "sha256": "acb5560af98adc58a474b3aa9fd2280a803c10e58538fb31f4c7cbd6be8518e2"
8465
+ "sha256": "d776fedff4ca4e2858592181e4d5afc71ea7cb5aef553466f831117d0dd2a435"
8501
8466
  },
8502
8467
  {
8503
8468
  "path": "package.json",
8504
8469
  "mode": "0000644",
8505
- "sha256": "dd99e950d328fee80b5a502201e1a2193d15590a5fe489645beca54695f9a774"
8470
+ "sha256": "0bc6e929e86642d34b2a0c1655ca4926adbbb2089d7ed683bb5792c06dee5fc4"
8506
8471
  },
8507
8472
  {
8508
8473
  "path": "public-docs/index.html",
@@ -8522,7 +8487,7 @@
8522
8487
  {
8523
8488
  "path": "release-notes.json",
8524
8489
  "mode": "0000644",
8525
- "sha256": "e0a7dbc41f8706f31363a5ef24aed64d4176f23edb66c72e99911a58beee7835"
8490
+ "sha256": "40a0e88364bef2a5e7fadeaa8f48b8ac8fa9b49a5e113e0c28ff1065fd116515"
8526
8491
  },
8527
8492
  {
8528
8493
  "path": "scripts/bongos-mcp.js",
@@ -9532,7 +9497,7 @@
9532
9497
  {
9533
9498
  "path": "scripts/gds/publish-manifest.js",
9534
9499
  "mode": "0000644",
9535
- "sha256": "dc5845faf4042a4272b2fbc72d8d62bb6f95b9f860d9501d3f5e710e47e910e3"
9500
+ "sha256": "17cb93e59d7f05319d8245fcbe9e55d5821c559680ce65f28fef763afece3b4c"
9536
9501
  },
9537
9502
  {
9538
9503
  "path": "scripts/gds/publish-watch.js",
@@ -10217,7 +10182,7 @@
10217
10182
  {
10218
10183
  "path": "scripts/setup-builder.sh",
10219
10184
  "mode": "0000644",
10220
- "sha256": "5860bf9daef3dd83a5ff4d3ee5e017e3cbd465ebda0c00df4ca622d629468ec4"
10185
+ "sha256": "f449553cc1965f6801e7a72a1dc90724ad7556eb047834c73c77ae9ab7ae2f76"
10221
10186
  },
10222
10187
  {
10223
10188
  "path": "scripts/status-mirror-sync.js",
@@ -10387,7 +10352,7 @@
10387
10352
  {
10388
10353
  "path": "src/bongos/module-scope-map.js",
10389
10354
  "mode": "0000644",
10390
- "sha256": "a5510df1cd4704db5d7c0a3c381ecdf591c9fa7b6ed6bd4fddcfd002611e288c"
10355
+ "sha256": "8f15e21380fe09d31b1a19d98415e502c81e41e566eda2fc9ec2496d8044eae9"
10391
10356
  },
10392
10357
  {
10393
10358
  "path": "src/bongos/module-submissions.js",
@@ -10592,7 +10557,7 @@
10592
10557
  {
10593
10558
  "path": "src/module-api.js",
10594
10559
  "mode": "0000644",
10595
- "sha256": "2be8a6906c0dd4d9d3e4af313c25e885abdeafe0f917399d1775c3692ce64c8f"
10560
+ "sha256": "b90170c38222c5a35bb87452ad97fa2f1289ad89d30338b4843ac4daced0cecd"
10596
10561
  },
10597
10562
  {
10598
10563
  "path": "src/module-loader/catalog.js",
package/docs/file-map.md CHANGED
@@ -17,14 +17,6 @@
17
17
  ├── CODEOWNERS ← owner review on the merge-gate surfaces (real since task 1002611, ADR 0159); inert until branch protection is on (scripts/gds/gating.js) — keep aligned with gate-review.js HARD_FLOOR_GLOBS
18
18
  ├── server.js ← game boot: Express + Colyseus + the shared internal surface (mountInternalSurfaces), binds 127.0.0.1:3000
19
19
  │
20
- ├── .devcontainer/ ← the Example Dev Box, defined as code (ADR 0031 §3) — builds identically on a DO box + a laptop
21
- │ ├── devcontainer.json ← box spec: features (Node 22, gh, Claude Code), NET_ADMIN caps, persisted volumes, lifecycle hooks
22
- │ ├── Dockerfile ← Ubuntu 24.04 base + Python 3.12 venv + firewall/art system deps + build-time integrity assert
23
- │ ├── init-firewall.sh ← postStart default-deny outbound firewall, tight allow-list (ADR 0031 §6)
24
- │ ├── post-create.sh ← first-run: folds in setup-builder.sh --no-auth + art deps + full-stack verify
25
- │ ├── healthcheck.sh ← image-level toolchain integrity check (the image half of doctor-as-health-check)
26
- │ └── README.md ← box docs, ADR mapping, deliberate reconciliations, out-of-scope tasks
27
- │
28
20
  ├── limitations/ ← versioning-methodology artifacts
29
21
  │ ├── README.md
30
22
  │ ├── v1-shipped.md ← V1 game archive (shipped 2026-05-12)
@@ -150,7 +142,7 @@
150
142
  │ ├── migrate.sh ← idempotent migration runner; applies core `migrations/*.sql` always + `modules/<key>/migrations/*.sql` only when the module is enabled (ADR 0083 §module-owned-migrations); stem-keyed so relocating a migration between dirs never re-runs it on prod
151
143
  │ ├── verify-gds-only-schema.sh ← task 1191: throwaway-DB proof that a Bongos-only migration run yields the build-system schema with NO game tables (needs local Postgres; not the DB-free unit gate)
152
144
  │ ├── setup-builder.js ← one-command new-builder bootstrap (toolchain + npm ci + .env.local + Bongos auth); cross-platform node entry (#673)
153
- │ ├── setup-builder.sh ← thin bash shim → setup-builder.js (Mac/Linux convenience + devcontainer/README refs)
145
+ │ ├── setup-builder.sh ← thin bash shim → setup-builder.js (Mac/Linux convenience + README refs)
154
146
  │ ├── deploy/ ← deploy scripts that run ON the droplet
155
147
  │ │ ├── deploy-prod.sh ← #1026/ADR 0056: version-controlled MIRROR of the droplet's hand-maintained ~/deploy.sh (byte-faithful). NOT auto-installed — the ci deploy key is forced-command-locked (ADR 0042/0043); reinstall by hand on change. Localhost /healthz retried 15×@1s so a slow bind no longer false-fails a deploy.
156
148
  │ │ ├── deploy-staging.sh ← #261: staging analogue of ~/deploy.sh; scp'd to ~/deploy-staging.sh + run by deploy-staging.yml on push to `staging` (same healthz retry)
@@ -2585,5 +2585,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2585
2585
  landed since 1.19.1047 with no explicit bump. run 36285856717. (task 1002620)
2586
2586
  1.19.1049 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2587
2587
  landed since 1.19.1048 with no explicit bump. run 36323359891. (task 1002620)
2588
+ 1.19.1050 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2589
+ landed since 1.19.1049 with no explicit bump. run 36324195800. (task 1002620)
2588
2590
  ---------------------------------------------------------------------------
2589
2591
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1049",
3
+ "version": "1.19.1050",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1049",
9
+ "version": "1.19.1050",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1049",
3
+ "version": "1.19.1050",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -7595,5 +7595,11 @@
7595
7595
  "id": "1003969",
7596
7596
  "text": "cloudbongos.com now checks for a new version every 15 minutes instead of once a day, so merged work goes live within about a quarter of an hour."
7597
7597
  }
7598
+ ],
7599
+ "1.19.1050": [
7600
+ {
7601
+ "id": "1003896",
7602
+ "text": "Removed the leftover dev-box container setup and the automated check that built it, so nothing in the project still describes or builds the retired dev box environment."
7603
+ }
7598
7604
  ]
7599
7605
  }
@@ -144,7 +144,6 @@ const PUBLISH_ALLOWLIST = [
144
144
  'docker-compose.yml',
145
145
  'docker-entrypoint.sh',
146
146
  '.dockerignore',
147
- '.devcontainer/',
148
147
  '.env.local.example',
149
148
  '.gitignore',
150
149
  '.gitattributes',
@@ -3,9 +3,9 @@
3
3
  #
4
4
  # The bootstrap logic now lives in scripts/setup-builder.js (Node), so it runs
5
5
  # on Windows/macOS/Linux alike. This shim stays so existing references keep
6
- # working on Mac/Linux — README, the onboarding primer, and the devcontainer's
7
- # post-create.sh all invoke `./scripts/setup-builder.sh` / `bash scripts/
8
- # setup-builder.sh ...`. It simply forwards all arguments to the Node script.
6
+ # working on Mac/Linux — README and the onboarding primer invoke
7
+ # `./scripts/setup-builder.sh` / `bash scripts/setup-builder.sh ...`. It simply
8
+ # forwards all arguments to the Node script.
9
9
  #
10
10
  # On Windows (no Git Bash), run the Node entry directly instead:
11
11
  # node scripts/setup-builder.js [--skip-keys] [--no-auth] [--help]
@@ -404,27 +404,14 @@ const MODULE_GLOBS = {
404
404
  // The SELF-HOST IMAGE (V4.R63, task 1202, ADR 0062): `docker compose up` brings
405
405
  // up Postgres + a vanilla, neutral-branded instance with every feature module
406
406
  // off. Assigned here by task 1004146 — same class as the .github/ and .husky/
407
- // entries above and the .devcontainer/ one below: repo-root build config that
408
- // defines WHAT RUNS. Not
407
+ // entries above: repo-root build config that defines WHAT RUNS. Not
409
408
  // `provisioning`, which owns the control plane that stands up instances, not an
410
- // image a stranger runs. They are not dev-box artifacts either, despite most
411
- // inbound references coming from .devcontainer/ and the devcontainer workflow.
409
+ // image a stranger runs.
412
410
  'Dockerfile', 'docker-compose.yml', 'docker-entrypoint.sh', '.dockerignore',
413
411
  // Repo-level dotfiles, the class .gitattributes and .npmrc already sit in.
414
412
  // .env.local.example is the local-secrets template, next to the .gitleaks.toml
415
413
  // scanner config above (task 1004146).
416
414
  '.gitignore', '.env.local.example',
417
- // .devcontainer/ is the same class as the entries above it — repo-level
418
- // developer-environment config, not a feature's territory (task 1004029).
419
- // It belonged to NO module and was not in SHARED_GLOBS, so isCovered() said
420
- // false for it under EVERY possible scope_modules value: no goal could
421
- // authorize a task that touched it, which is what stalled task 1003896
422
- // (delete .devcontainer/). Given to devsecops rather than added to
423
- // SHARED_GLOBS because it has a real owner — SHARED_GLOBS is for roots owned
424
- // by nobody, and every addition there widens the wall for every goal at once.
425
- // devsecops is PROTECTED, so this path is now Archon-only to scope; that is
426
- // the correct floor for a file that defines what a builder's environment runs.
427
- '.devcontainer/',
428
415
  'package.json', 'package-lock.json',
429
416
  'scripts/gds/fitness.js', 'scripts/gds/docs-entropy.js',
430
417
  'scripts/gds/redteam-patrol.js', 'scripts/gds/audit-gate.js',
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1049'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1050'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -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
@@ -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."
@@ -1,149 +0,0 @@
1
- #!/usr/bin/env bash
2
- # post-create.sh — first-run provisioning for the Example Dev Box.
3
- #
4
- # Runs once, as the `vscode` user, after the container is created and the repo is
5
- # mounted at /workspace (devcontainer.json: postCreateCommand). This is where
6
- # ADR 0031 §3's "fold setup-builder.sh into the container's first-run step"
7
- # actually happens:
8
- #
9
- # 1. scripts/setup-builder.sh --no-auth --skip-keys
10
- # → verifies the toolchain, runs `npm ci`, seeds .env.local from the
11
- # (secret-free) example. The --no-auth flag skips the interactive
12
- # GitHub Device Flow, which can't run in a non-interactive postCreate —
13
- # the builder authenticates on first attach instead (see the banner).
14
- # 2. pip install the pinned art-pipeline deps INTO the venv the image created.
15
- # → setup-builder.sh doesn't touch Python deps; the container layer owns
16
- # that, installing from the repo's manifest so it stays reproducible.
17
- # 3. A full-stack verification (sharp loads, art libs import) so a green
18
- # first-run means the box is genuinely ready, not just "built".
19
- #
20
- # No secrets are written here (ADR 0022): .env.local is copied from the example
21
- # with blank placeholders; the per-builder Gemini key and GDS session arrive at
22
- # runtime via the mounted ~/.config/otb volume and /builder-setup.
23
-
24
- set -uo pipefail
25
-
26
- cd /workspace 2>/dev/null || cd "$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
27
-
28
- bold=$'\033[1m'; green=$'\033[32m'; yellow=$'\033[33m'; dim=$'\033[2m'; reset=$'\033[0m'
29
- say() { echo "${bold}$*${reset}"; }
30
-
31
- say "Example Dev Box — first-run provisioning"
32
- echo
33
-
34
- # git trusts the mounted workspace even though its owner differs from the
35
- # container user (avoids "detected dubious ownership" on every git command).
36
- git config --global --add safe.directory /workspace 2>/dev/null || true
37
-
38
- # node_modules is a container-isolated named volume (see devcontainer.json). A
39
- # fresh volume mounts root-owned, so hand it to the runtime user before the
40
- # npm ci inside setup-builder.sh tries to populate it.
41
- if [ -d node_modules ]; then
42
- sudo chown vscode:vscode node_modules 2>/dev/null || true
43
- fi
44
-
45
- # --- 1. Bring up the firewall BEFORE installing dependencies ------------------
46
- # postStartCommand also runs the firewall on every start, but that's AFTER this
47
- # postCreate step — which would leave `npm ci` + `pip install` below running with
48
- # unrestricted egress on first create, a window a malicious (post)install script
49
- # could exfiltrate through. Apply default-deny up front so dependency installs
50
- # happen behind the allow-list (npm + PyPI + GitHub are allow-listed, so they
51
- # still work). Non-fatal: if it can't apply (e.g. a laptop without NET_ADMIN),
52
- # postStart will report it; we don't block provisioning here.
53
- say "1/4 Securing egress (default-deny firewall, before dependency installs)"
54
- if sudo /usr/local/bin/otb-init-firewall.sh; then
55
- echo " ${green}ok${reset} firewall active — installs run behind the allow-list"
56
- else
57
- echo " ${yellow}!!${reset} firewall did not apply (see above); dependency installs will run with open egress this once"
58
- fi
59
- echo
60
-
61
- # --- 2. Fold in setup-builder.sh (toolchain + npm ci + .env.local) ------------
62
- say "2/4 Toolchain + dependencies (scripts/setup-builder.sh --no-auth)"
63
- if [ -x scripts/setup-builder.sh ]; then
64
- if bash scripts/setup-builder.sh --no-auth --skip-keys; then
65
- echo " ${green}ok${reset} setup-builder.sh completed"
66
- else
67
- echo " ${yellow}!!${reset} setup-builder.sh reported a problem — review the output above"
68
- fi
69
- else
70
- echo " ${yellow}!!${reset} scripts/setup-builder.sh not found/executable — running npm ci directly"
71
- npm ci --no-audit --no-fund || echo " ${yellow}!!${reset} npm ci failed"
72
- fi
73
- echo
74
-
75
- # --- 2. Art pipeline Python deps into the venv --------------------------------
76
- # Non-fatal: a box without art deps is still fully usable for game/GDS work.
77
- # Retry a few times — on a macOS Docker Desktop bind mount, VirtioFS can
78
- # transiently return EDEADLK ("Resource deadlock avoided") reading repo files;
79
- # a retry (and a fresh container) usually clears it. The DO box (#598) has no
80
- # bind mount, so this never bites there.
81
- say "3/4 Art-pipeline Python deps (into ${OTB_VENV:-venv})"
82
- if [ -f modules/art-pipeline/pipeline/requirements.txt ]; then
83
- art_ok=0
84
- for attempt in 1 2 3; do
85
- if pip install --no-cache-dir -r modules/art-pipeline/pipeline/requirements.txt; then
86
- echo " ${green}ok${reset} art deps installed"
87
- art_ok=1
88
- break
89
- fi
90
- echo " ${dim}--${reset} attempt ${attempt} failed; retrying…"
91
- done
92
- if [ "$art_ok" -eq 0 ]; then
93
- echo " ${yellow}!!${reset} pip install failed after retries — the art pipeline won't run until resolved."
94
- echo " On a macOS laptop this is usually Docker Desktop's VirtioFS (EDEADLK):"
95
- echo " rebuild the container, or switch Docker Desktop → Settings → General →"
96
- echo " file sharing to 'gRPC FUSE'. (The box is otherwise ready.)"
97
- fi
98
- else
99
- echo " ${dim}--${reset} no modules/art-pipeline/pipeline/requirements.txt; skipping"
100
- fi
101
- echo
102
-
103
- # --- 3. Full-stack verification -----------------------------------------------
104
- say "4/4 Verifying the box is ready"
105
- # sharp was verified here as "the native image dep". Task 1003207 removed it: its
106
- # only consumers were the game-era snapshot renderers and an art-pipeline test,
107
- # all of which left this core with the game. Nothing native remains to prove.
108
- # The five art libs must import in the venv.
109
- if python3 -c "import PIL, numpy, scipy, sklearn, certifi" >/dev/null 2>&1; then
110
- echo " ${green}ok${reset} art libs import (Pillow, numpy, scipy, scikit-learn, certifi)"
111
- else
112
- echo " ${yellow}!!${reset} one or more art libs failed to import"
113
- fi
114
- echo
115
-
116
- # --- Claude Code default permission mode (task 1251) --------------------------
117
- # Match the host (infra/box-source-fetch.sh): default this container's interactive
118
- # Claude Code to bypassPermissions so `claude` never stops for a prompt on the
119
- # (isolated, often unattended) dev box. USER-level (~/.claude/settings.json — the
120
- # persisted otb-claude-config volume), NEVER the repo's project .claude/settings.json,
121
- # so laptops / local clones are unaffected; it merges with the project settings
122
- # (which set no defaultMode). Idempotent — only sets the one key, preserving the rest.
123
- say "Defaulting Claude Code to bypassPermissions (box-only ~/.claude/settings.json)"
124
- if mkdir -p "$HOME/.claude" 2>/dev/null && node -e '
125
- const fs = require("fs"), p = process.argv[1];
126
- let s = {}; try { s = JSON.parse(fs.readFileSync(p, "utf8")) || {}; } catch (e) {}
127
- if (typeof s !== "object" || s === null || Array.isArray(s)) s = {};
128
- s.permissions = (s.permissions && typeof s.permissions === "object") ? s.permissions : {};
129
- s.permissions.defaultMode = "bypassPermissions";
130
- fs.writeFileSync(p, JSON.stringify(s, null, 2) + "\n");
131
- ' "$HOME/.claude/settings.json" 2>/dev/null; then
132
- echo " ${green}ok${reset} defaultMode=bypassPermissions written"
133
- else
134
- echo " ${yellow}!!${reset} could not write ~/.claude/settings.json (claude will prompt normally)"
135
- fi
136
- echo
137
-
138
- say "${green}Box provisioned.${reset}"
139
- cat <<'BANNER'
140
-
141
- Next, on first connect:
142
- 1. Authenticate the GDS CLI: node scripts/gds/setup.js (or /builder-setup)
143
- 2. See what you can claim: /builder-start
144
-
145
- The pixel-art pipeline needs a Google AI Studio key in ~/.config/otb/env
146
- (that path is a persisted volume, so it survives box rebuilds). Skip if you
147
- are not running the art pipeline.
148
-
149
- BANNER