@bongos/core 1.19.319 → 1.19.320

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.
Files changed (47) hide show
  1. package/.bongos-core.json +121 -36
  2. package/.claude/skills/merge-mode/SKILL.md +3 -1
  3. package/.devcontainer/init-firewall.sh +41 -5
  4. package/clients/bongos-client/examples/hello-world.mjs +11 -2
  5. package/docs/copy-inventory.md +20 -20
  6. package/docs/copy-registry.json +22 -22
  7. package/modules/hall-ui/public/builders.js +0 -1
  8. package/package-lock.json +2 -2
  9. package/package.json +1 -1
  10. package/scripts/gds/audit-replay.sh +20 -3
  11. package/scripts/gds/box.js +79 -86
  12. package/scripts/gds/db-backup-nightly.sh +17 -4
  13. package/scripts/gds/db-backup-offsite.sh +23 -6
  14. package/scripts/gds/discipline-modes.json +1 -1
  15. package/scripts/gds/doc-cli-guard.js +34 -2
  16. package/scripts/gds/fitness.js +8 -4
  17. package/scripts/gds/gds-db-backup.service +15 -4
  18. package/scripts/gds/gds-db-backup.timer +12 -1
  19. package/scripts/gds/gds-db-offsite.service +16 -5
  20. package/scripts/gds/gds-db-offsite.timer +12 -1
  21. package/scripts/gds/instance-apex.js +28 -0
  22. package/scripts/gds/newcomer-floor.templates.json +2 -2
  23. package/scripts/gds/provision.js +7 -3
  24. package/scripts/gds/ship-deploy-target.js +388 -0
  25. package/scripts/gds/ship-finish.js +377 -0
  26. package/scripts/gds/ship-flow.js +814 -0
  27. package/scripts/gds/ship-git.js +100 -0
  28. package/scripts/gds/ship-grade-steps.js +376 -0
  29. package/scripts/gds/ship-grade.js +319 -0
  30. package/scripts/gds/ship-io.js +100 -0
  31. package/scripts/gds/ship-land.js +539 -0
  32. package/scripts/gds/ship-memory-sync.js +192 -0
  33. package/scripts/gds/ship-merge.js +297 -0
  34. package/scripts/gds/ship-preflight-steps.js +323 -0
  35. package/scripts/gds/ship-regen.js +286 -0
  36. package/scripts/gds/ship-resume.js +151 -0
  37. package/scripts/gds/ship-session-upload.js +216 -0
  38. package/scripts/gds/ship-state.js +73 -0
  39. package/scripts/gds/ship-surprise.js +84 -0
  40. package/scripts/gds/ship.js +105 -4195
  41. package/scripts/status-probe.mjs +30 -2
  42. package/src/module-api.js +3 -1
  43. package/tests/helpers.mjs +13 -0
  44. package/tests/provision.mjs +7 -1
  45. package/tests/regrade_eligibility.mjs +43 -0
  46. package/tests/ship_assurance.mjs +4 -2
  47. package/tests/ship_publish_error.mjs +43 -0
@@ -764,7 +764,7 @@
764
764
  "text": "No achievements earned yet.",
765
765
  "occurrences": 2,
766
766
  "at": [
767
- "modules/hall-ui/public/builders.js:1287",
767
+ "modules/hall-ui/public/builders.js:1286",
768
768
  "modules/hall-ui/public/profile.js:108"
769
769
  ]
770
770
  },
@@ -1114,7 +1114,7 @@
1114
1114
  "text": "Won {…}",
1115
1115
  "occurrences": 2,
1116
1116
  "at": [
1117
- "modules/hall-ui/public/builders.js:1294",
1117
+ "modules/hall-ui/public/builders.js:1293",
1118
1118
  "modules/hall-ui/public/profile.js:115"
1119
1119
  ]
1120
1120
  },
@@ -1784,7 +1784,7 @@
1784
1784
  "surface": "builders-hall",
1785
1785
  "text": "Achievements could not be loaded.",
1786
1786
  "file": "modules/hall-ui/public/builders.js",
1787
- "line": 2317,
1787
+ "line": 2316,
1788
1788
  "origin": "js-markup",
1789
1789
  "confidence": "certain"
1790
1790
  },
@@ -1820,7 +1820,7 @@
1820
1820
  "surface": "builders-hall",
1821
1821
  "text": "All builders",
1822
1822
  "file": "modules/hall-ui/public/builders.js",
1823
- "line": 1574,
1823
+ "line": 1573,
1824
1824
  "origin": "js-markup",
1825
1825
  "confidence": "certain"
1826
1826
  },
@@ -1901,7 +1901,7 @@
1901
1901
  "surface": "builders-hall",
1902
1902
  "text": "Analytics scope",
1903
1903
  "file": "modules/hall-ui/public/builders.js",
1904
- "line": 1572,
1904
+ "line": 1571,
1905
1905
  "origin": "js-markup",
1906
1906
  "confidence": "certain"
1907
1907
  },
@@ -3305,7 +3305,7 @@
3305
3305
  "surface": "builders-hall",
3306
3306
  "text": "Could not save just now. Try again.",
3307
3307
  "file": "modules/hall-ui/public/builders.js",
3308
- "line": 1271,
3308
+ "line": 1270,
3309
3309
  "origin": "js-notice",
3310
3310
  "confidence": "certain"
3311
3311
  },
@@ -3854,7 +3854,7 @@
3854
3854
  "surface": "builders-hall",
3855
3855
  "text": "Downvote (−1)",
3856
3856
  "file": "modules/hall-ui/public/builders.js",
3857
- "line": 1756,
3857
+ "line": 1755,
3858
3858
  "origin": "js-markup",
3859
3859
  "confidence": "certain"
3860
3860
  },
@@ -4673,7 +4673,7 @@
4673
4673
  "surface": "builders-hall",
4674
4674
  "text": "Ideas awaiting triage",
4675
4675
  "file": "modules/hall-ui/public/builders.js",
4676
- "line": 1876,
4676
+ "line": 1875,
4677
4677
  "origin": "js-markup",
4678
4678
  "confidence": "certain"
4679
4679
  },
@@ -4916,7 +4916,7 @@
4916
4916
  "surface": "builders-hall",
4917
4917
  "text": "Kinds of work shipped",
4918
4918
  "file": "modules/hall-ui/public/builders.js",
4919
- "line": 1624,
4919
+ "line": 1623,
4920
4920
  "origin": "js-markup",
4921
4921
  "confidence": "certain"
4922
4922
  },
@@ -4934,7 +4934,7 @@
4934
4934
  "surface": "builders-hall",
4935
4935
  "text": "Last 14 days: {…} / 10 average across {…} graded work{…} ({…} passed).",
4936
4936
  "file": "modules/hall-ui/public/builders.js",
4937
- "line": 1492,
4937
+ "line": 1491,
4938
4938
  "origin": "js-text-assign",
4939
4939
  "confidence": "certain"
4940
4940
  },
@@ -5312,7 +5312,7 @@
5312
5312
  "surface": "builders-hall",
5313
5313
  "text": "Metic rank and above may vote",
5314
5314
  "file": "modules/hall-ui/public/builders.js",
5315
- "line": 1760,
5315
+ "line": 1759,
5316
5316
  "origin": "js-markup",
5317
5317
  "confidence": "certain"
5318
5318
  },
@@ -5555,7 +5555,7 @@
5555
5555
  "surface": "builders-hall",
5556
5556
  "text": "No achievements earned yet.",
5557
5557
  "file": "modules/hall-ui/public/builders.js",
5558
- "line": 1287,
5558
+ "line": 1286,
5559
5559
  "origin": "js-markup",
5560
5560
  "confidence": "certain"
5561
5561
  },
@@ -7976,7 +7976,7 @@
7976
7976
  "surface": "builders-hall",
7977
7977
  "text": "The page could not be loaded. Try again.",
7978
7978
  "file": "modules/hall-ui/public/builders.js",
7979
- "line": 2555,
7979
+ "line": 2554,
7980
7980
  "origin": "js-notice",
7981
7981
  "confidence": "certain"
7982
7982
  },
@@ -8435,7 +8435,7 @@
8435
8435
  "surface": "builders-hall",
8436
8436
  "text": "Upvote (+1)",
8437
8437
  "file": "modules/hall-ui/public/builders.js",
8438
- "line": 1756,
8438
+ "line": 1755,
8439
8439
  "origin": "js-markup",
8440
8440
  "confidence": "certain"
8441
8441
  },
@@ -8498,7 +8498,7 @@
8498
8498
  "surface": "builders-hall",
8499
8499
  "text": "Vote on this work",
8500
8500
  "file": "modules/hall-ui/public/builders.js",
8501
- "line": 1756,
8501
+ "line": 1755,
8502
8502
  "origin": "js-markup",
8503
8503
  "confidence": "certain"
8504
8504
  },
@@ -8795,7 +8795,7 @@
8795
8795
  "surface": "builders-hall",
8796
8796
  "text": "Won {…}",
8797
8797
  "file": "modules/hall-ui/public/builders.js",
8798
- "line": 1294,
8798
+ "line": 1293,
8799
8799
  "origin": "js-markup",
8800
8800
  "confidence": "certain"
8801
8801
  },
@@ -9533,7 +9533,7 @@
9533
9533
  "surface": "builders-hall",
9534
9534
  "text": "fell short",
9535
9535
  "file": "modules/hall-ui/public/builders.js",
9536
- "line": 1469,
9536
+ "line": 1468,
9537
9537
  "origin": "js-markup",
9538
9538
  "confidence": "certain"
9539
9539
  },
@@ -9902,7 +9902,7 @@
9902
9902
  "surface": "builders-hall",
9903
9903
  "text": "no data",
9904
9904
  "file": "modules/hall-ui/public/builders.js",
9905
- "line": 1516,
9905
+ "line": 1515,
9906
9906
  "origin": "js-markup",
9907
9907
  "confidence": "certain"
9908
9908
  },
@@ -9920,7 +9920,7 @@
9920
9920
  "surface": "builders-hall",
9921
9921
  "text": "not graded (grader outage)",
9922
9922
  "file": "modules/hall-ui/public/builders.js",
9923
- "line": 1466,
9923
+ "line": 1465,
9924
9924
  "origin": "js-markup",
9925
9925
  "confidence": "certain"
9926
9926
  },
@@ -10316,7 +10316,7 @@
10316
10316
  "surface": "builders-hall",
10317
10317
  "text": "trend, latest {…}",
10318
10318
  "file": "modules/hall-ui/public/builders.js",
10319
- "line": 1528,
10319
+ "line": 1527,
10320
10320
  "origin": "js-markup",
10321
10321
  "confidence": "certain"
10322
10322
  },
@@ -10865,7 +10865,7 @@
10865
10865
  "surface": "builders-hall",
10866
10866
  "text": "— all builders —",
10867
10867
  "file": "modules/hall-ui/public/builders.js",
10868
- "line": 1692,
10868
+ "line": 1691,
10869
10869
  "origin": "js-markup",
10870
10870
  "confidence": "certain"
10871
10871
  },
@@ -10973,7 +10973,7 @@
10973
10973
  "surface": "builders-hall",
10974
10974
  "text": "…{…} more open ideas — triage clears them one at a time.",
10975
10975
  "file": "modules/hall-ui/public/builders.js",
10976
- "line": 1874,
10976
+ "line": 1873,
10977
10977
  "origin": "js-markup",
10978
10978
  "confidence": "certain"
10979
10979
  },
@@ -1240,7 +1240,6 @@
1240
1240
  }
1241
1241
  }
1242
1242
 
1243
-
1244
1243
  // V3 (2026-05-30): clear the pick_disciplines onboarding stage from the path
1245
1244
  // checklist. Saves the chosen craft via the same PATCH /me/disciplines route
1246
1245
  // the profile uses, then re-renders the checklist (and the profile picker) so
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.319",
3
+ "version": "1.19.320",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.319",
9
+ "version": "1.19.320",
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.319",
3
+ "version": "1.19.320",
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",
@@ -54,17 +54,34 @@ fi
54
54
  DROPLET="lars@104.236.254.243"
55
55
  PG_SOCKET="/var/run/postgresql"
56
56
 
57
+ # Database + role: explicit env wins, else the branding-pack resolver (task
58
+ # 1002757 — the founder DB/unix-user pair was baked in as runtime args). The
59
+ # role defaults to the CURRENT unix user (peer auth), matching migrate.sh.
60
+ # Resolved BEFORE any SSH re-invocation: the remote side has no repo to
61
+ # resolve from, so the value rides the wire in GDS_AUDIT_DB.
62
+ AUDIT_DB="${GDS_AUDIT_DB:-${PGDATABASE:-}}"
63
+ if [ -z "$AUDIT_DB" ]; then
64
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || true)"
65
+ [ -n "$SCRIPT_DIR" ] && AUDIT_DB="$(node -e "process.stdout.write(require(process.argv[1]).instanceDbName() || '')" "$SCRIPT_DIR/../../src/bongos/pool" 2>/dev/null || true)"
66
+ fi
67
+ if [ -z "$AUDIT_DB" ]; then
68
+ echo "audit-replay: cannot determine the database — set GDS_AUDIT_DB or PGDATABASE (refusing a baked-in default, task 1002757)." >&2
69
+ exit 2
70
+ fi
71
+
57
72
  if [ ! -S "$PG_SOCKET/.s.PGSQL.5432" ]; then
58
73
  # Re-invoke ourselves on the droplet. The script doesn't depend on any
59
74
  # repo file at runtime, so we ship the script body over stdin to avoid a
60
- # rsync step. Quoting on the SSH side preserves the timestamp args.
75
+ # rsync step. Quoting on the SSH side preserves the timestamp args; the
76
+ # locally-resolved DB name rides along (no repo on the remote side).
61
77
  exec ssh -o ConnectTimeout=10 "$DROPLET" \
62
- "bash -s -- '$BUILDER_ID' '$START' '$END'" < "$0"
78
+ "GDS_AUDIT_DB='$AUDIT_DB' bash -s -- '$BUILDER_ID' '$START' '$END'" < "$0"
63
79
  fi
64
80
 
65
81
  # From here down: running where Postgres is reachable on the Unix socket.
66
82
 
67
- PSQL_ARGS=(-h "$PG_SOCKET" -d amazonprimea -U lars -t -A -F $'\t' --no-psqlrc)
83
+ PSQL_ARGS=(-h "$PG_SOCKET" -d "$AUDIT_DB" -t -A -F $'\t' --no-psqlrc)
84
+ [ -n "${GDS_AUDIT_PG_USER:-}" ] && PSQL_ARGS+=(-U "$GDS_AUDIT_PG_USER")
68
85
 
69
86
  q() {
70
87
  PGOPTIONS='--client-min-messages=warning' psql "${PSQL_ARGS[@]}" -c "$1"
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/gds/box.js — per-builder cloud dev box lifecycle CLI (ADR 0031 §2/§5, #598).
3
3
  //
4
- // The control-plane entrypoint for the box lifecycle. Like provision-staging.sh,
5
- // it is meant to RUN ON A TRUSTED CONTROL PLANE (the prod droplet, or an
6
- // operator's machine) — NOT inside a builder box. A builder box provisioning
7
- // boxes would be a privilege-escalation; the builder firewall doesn't even
8
- // allow-list the DO API. The rank gate is enforced here against the builder's
9
- // LIVE DB rank (the same value requireRank reads), so it is a real ADR-0016
4
+ // The control-plane entrypoint for the box lifecycle. Like provision-staging.sh it
5
+ // must RUN ON A TRUSTED CONTROL PLANE (the prod droplet or an operator's machine),
6
+ // NOT inside a builder box — a box provisioning boxes is privilege escalation, and
7
+ // the builder firewall doesn't even allow-list the DO API. The rank gate reads the
8
+ // builder's LIVE DB rank (what requireRank reads), so it is a real ADR-0016
10
9
  // server-side check, not local prose.
11
10
  //
12
11
  // Subcommands:
@@ -21,10 +20,9 @@
21
20
  //
22
21
  // <builder> is a GitHub login or a numeric builder id.
23
22
  //
24
- // SAFETY: dry-run is the DEFAULT. Nothing touches DigitalOcean or writes the DB
25
- // unless you pass --apply. Run any command once without --apply to see the plan,
26
- // exactly like `pulse.js --release-stale --dry-run`.
27
- //
23
+ // SAFETY: dry-run is the DEFAULT — nothing touches DigitalOcean or writes the DB
24
+ // without --apply, so run any command bare first to see the plan (as with
25
+ // `pulse.js --release-stale --dry-run`):
28
26
  // node scripts/gds/box.js sweep-idle # plan only
29
27
  // node scripts/gds/box.js sweep-idle --apply # actually park idle boxes
30
28
  //
@@ -96,6 +94,9 @@ function resolveBoxInfraDir(opts = {}) {
96
94
  return null;
97
95
  }
98
96
 
97
+ // task 1002757 — the instance's own apex; null when unconfigured (callers refuse).
98
+ const { instanceApex } = require('./instance-apex');
99
+
99
100
  const CONFIG = {
100
101
  floor: process.env.BOX_PROVISION_MIN_RANK || 'xenos',
101
102
  region: process.env.BOX_REGION || 'nyc3',
@@ -108,34 +109,35 @@ const CONFIG = {
108
109
  sshKeyIds: (process.env.BOX_SSH_KEY_IDS || '').split(',').map((s) => s.trim()).filter(Boolean),
109
110
  dnsHook: process.env.BOX_DNS_HOOK || null,
110
111
  tag: process.env.BOX_TAG || 'otb-builder-box',
111
- dnsZone: process.env.BOX_DNS_ZONE || 'dev.amazonprimea.com',
112
+ // task 1002757 settled this file's old carve-out: an operator-plane DNS zone is
113
+ // still a FUNCTIONAL DEFAULT, and a founder literal aimed another operator's
114
+ // fleet at the founder's zone. Derived from the instance apex, null when
115
+ // unconfigured — hostnameFor() then refuses instead of minting a dead FQDN.
116
+ dnsZone: process.env.BOX_DNS_ZONE || (instanceApex() ? `dev.${instanceApex()}` : null),
112
117
  };
113
118
 
114
119
  // Named-tunnel automation for the browser terminal (#771, ADR 0038 Part B).
115
120
  // OPT-IN, default OFF — an unset BOX_NAMED_TUNNEL leaves the box on the tokenless
116
- // quick tunnel (trycloudflare.com), the zero-secret #760 default. When the
117
- // operator turns it on AND supplies a Cloudflare token with Tunnel + DNS scope
118
- // (the same CLOUDFLARE_API_TOKEN box-dns uses, scope added) + the account id, every
119
- // provision creates a stable term-<login>.dev.amazonprimea.com tunnel and bakes
120
- // its connector token into the box; deprovision tears it down.
121
+ // quick tunnel (trycloudflare.com), the zero-secret #760 default. When the operator
122
+ // turns it on AND supplies a Cloudflare token with Tunnel + DNS scope (the same
123
+ // CLOUDFLARE_API_TOKEN box-dns uses) + the account id, every provision creates a
124
+ // stable term-<login>.<terminalZone> tunnel and bakes its connector token into the
125
+ // box; deprovision tears it down.
121
126
  const CF = {
122
127
  enabled: /^(1|true|yes)$/i.test(process.env.BOX_NAMED_TUNNEL || ''),
123
128
  apiToken: process.env.CLOUDFLARE_API_TOKEN || null,
124
129
  accountId: process.env.CLOUDFLARE_ACCOUNT_ID || null,
125
- // The Cloudflare ZONE the DNS record lives in (registrable apex), e.g.
126
- // amazonprimea.com — term-<login>.dev.amazonprimea.com is a record WITHIN it,
127
- // matching box-dns-cloudflare.sh. Distinct from terminalZone (the host suffix).
128
- zone: process.env.CLOUDFLARE_ZONE || 'amazonprimea.com',
129
- // The host suffix the terminal hostname is built on (term-<login>.<terminalZone>).
130
- // DEFAULTS TO THE APEX, NOT dev.* — the terminal rides a PROXIED Cloudflare
131
- // tunnel (HTTPS at the edge), and free Universal SSL covers `amazonprimea.com` +
132
- // `*.amazonprimea.com` (ONE label) but NOT `term-x.dev.amazonprimea.com` (TWO
133
- // labels) → a 2-level host fails TLS at the edge (verified live, #771). One
134
- // level (term-<login>.amazonprimea.com) is covered with no paid cert. An
135
- // operator with an Advanced Certificate for *.dev.amazonprimea.com can override
136
- // via BOX_TERMINAL_ZONE. (The SSH A-record path, box-dns-cloudflare.sh, stays on
137
- // dev.* — it is proxied=false / direct-IP, so it needs no edge cert.)
138
- terminalZone: process.env.BOX_TERMINAL_ZONE || 'amazonprimea.com',
130
+ // The Cloudflare ZONE the record lives in (the registrable apex) — the terminal
131
+ // host is a record WITHIN it, matching box-dns-cloudflare.sh. Distinct from
132
+ // terminalZone (the host suffix). Both resolve from the instance (task 1002757).
133
+ zone: process.env.CLOUDFLARE_ZONE || instanceApex(),
134
+ // Host suffix for term-<login>.<terminalZone>. DEFAULTS TO THE APEX, NOT dev.*:
135
+ // the terminal rides a PROXIED tunnel, and free Universal SSL covers <apex> +
136
+ // *.<apex> (ONE label) but not a TWO-label host like term-x.dev.<apex>, which
137
+ // fails TLS at the edge (verified live, #771). An operator holding an Advanced
138
+ // Certificate for *.dev.<apex> can override via BOX_TERMINAL_ZONE. (The SSH
139
+ // A-record path stays on dev.* — proxied=false, so it needs no edge cert.)
140
+ terminalZone: process.env.BOX_TERMINAL_ZONE || instanceApex(),
139
141
  };
140
142
 
141
143
  // Named-tunnel automation is live only when the operator opted in AND the
@@ -158,10 +160,9 @@ function makeCf(apply) {
158
160
  // SECURITY PREFLIGHT (task 1002511, ADR 0053 / SR#11). The edge cookie-strip
159
161
  // Transform Rule is applied BEST-EFFORT deep inside ensureNamedTunnel, so a
160
162
  // CLOUDFLARE_API_TOKEN missing the zone "Transform Rules" edit scope fails as one
161
- // buried log line — and the apex session cookie keeps reaching the box subdomains.
162
- // This probe runs BEFORE any DigitalOcean spend (dry-run included; a read-only
163
- // lookup running for real in a plan is the same precedent as the drift reconcile's
164
- // live droplet listing) and shouts the exact fix.
163
+ // buried log line while the apex session cookie keeps reaching box subdomains. This
164
+ // probe runs BEFORE any DigitalOcean spend (dry-run included — a read-only lookup in
165
+ // a plan is the drift-reconcile precedent) and shouts the exact fix.
165
166
  //
166
167
  // HONESTY: the probe is a GET, so it can only DISPROVE the scope. A readable phase
167
168
  // does NOT mean the rule can be written — only the PUT inside ensureNamedTunnel
@@ -278,18 +279,21 @@ function shortStamp() {
278
279
  }
279
280
 
280
281
  function dropletName(login) { return `otb-box-${login}`; }
281
- function hostnameFor(login) { return `box-${login}.${CONFIG.dnsZone}`; }
282
-
283
- // Run the optional DNS hook so the per-builder hostname tracks the new IP. When
284
- // no hook is configured, just print the reminder — the stable-hostname CUTOVER
285
- // is #599's (Desktop managed-settings); #598 only records IP + hostname + offers
286
- // the hook so an operator can wire DO/Cloudflare DNS without editing this file.
287
- // Returns { ok, error?, skipped? }. task 1180: callers MUST act on ok:false
288
- // (record a box_events row + flag the box) instead of silently continuing — the
289
- // swallow that left @kingaonline's A record pinned at a dead IP while provision
290
- // reported success. The hook itself now retries transient Cloudflare blips
291
- // (infra/box-dns-cloudflare.sh), so ok:false here is a real, exhausted failure.
292
- // `hook` is injectable so the contract can be unit-tested with a fake (#1034).
282
+ // Refuses on an unresolved zone (task 1002757) — `box-x.undefined` would mint a
283
+ // DNS record and a box that can never be reached.
284
+ function hostnameFor(login) {
285
+ if (!CONFIG.dnsZone) throw new Error('box: no DNS zone configured — set BOX_DNS_ZONE, or give this instance a domains.publicOrigin in config/branding.json (task 1002757).');
286
+ return `box-${login}.${CONFIG.dnsZone}`;
287
+ }
288
+
289
+ // Run the optional DNS hook so the per-builder hostname tracks the new IP; with no
290
+ // hook configured, print the reminder — the stable-hostname CUTOVER is #599's, #598
291
+ // only records IP + hostname and offers the hook so an operator can wire DNS without
292
+ // editing this file. → { ok, error?, skipped? }. task 1180: callers MUST act on
293
+ // ok:false (box_events row + flag the box), never continue silently — that swallow
294
+ // left @kingaonline's A record pinned at a dead IP while provision reported success.
295
+ // The hook retries transient Cloudflare blips, so ok:false is a real, exhausted
296
+ // failure. `hook` is injectable so the contract is unit-testable (#1034).
293
297
  function runDnsHook(hostname, ip, hook = CONFIG.dnsHook) {
294
298
  if (!ip) return { ok: true, skipped: true };
295
299
  if (!hook) {
@@ -316,21 +320,16 @@ function runDnsHook(hostname, ip, hook = CONFIG.dnsHook) {
316
320
  // dead end to park a record at.
317
321
  const DNS_BLACKHOLE_IP = '192.0.2.1';
318
322
 
319
- // Park the per-builder A record at the blackhole sentinel on deprovision (idea
320
- // 309, refining task 1180). 1180 DELETED the record on teardown so it couldn't
321
- // dangle at a recycled DO IP — but delete-then-recreate across a reap→rebuild
322
- // left a window where the name returned NXDOMAIN, which resolvers negative-cache
323
- // for the zone's SOA neg-TTL (~30 min on the Cloudflare amazonprimea.com zone,
324
- // not builder-editable). A client that queried during that window (e.g. an open
325
- // Claude Desktop) then kept getting ENOTFOUND long after the box was back and the
326
- // record recreated. REPOINTING to a non-routable sentinel instead keeps the name
327
- // always-resolving (no NXDOMAIN → nothing to negative-cache) while preserving
328
- // 1180's guarantee that a torn-down box's name can never dial a recycled
329
- // stranger's IP. The next provision/wake PUTs the record straight to the live IP
330
- // — a clean in-place repoint at the 60s record TTL, no gap. Best-effort (like the
331
- // prior delete): a failure just leaves the old IP, which the next provision
332
- // repoints anyway. Reuses the UPSERT path of runDnsHook; `hook` is injectable for
333
- // tests. Returns { ok, error?, skipped? }.
323
+ // Park the per-builder A record at the blackhole sentinel on deprovision (idea 309,
324
+ // refining task 1180). 1180 DELETED the record so it couldn't dangle at a recycled
325
+ // DO IP, but delete-then-recreate across a reap→rebuild left an NXDOMAIN window that
326
+ // resolvers negative-cache for the zone's SOA neg-TTL (~30 min, not builder-editable)
327
+ // — a client querying then kept getting ENOTFOUND long after the box was back.
328
+ // REPOINTING to a non-routable sentinel keeps the name always-resolving (nothing to
329
+ // negative-cache) while preserving 1180's guarantee that a torn-down box's name can
330
+ // never dial a stranger's recycled IP; the next provision/wake PUTs the live IP
331
+ // in-place at the 60s TTL, no gap. Best-effort like the prior delete. Reuses
332
+ // runDnsHook's UPSERT path; `hook` injectable for tests. → { ok, error?, skipped? }
334
333
  function runDnsHookBlackhole(hostname, hook = CONFIG.dnsHook) {
335
334
  if (!hook || !hostname) return { ok: true, skipped: true };
336
335
  return runDnsHook(hostname, DNS_BLACKHOLE_IP, hook);
@@ -510,14 +509,13 @@ async function cmdProvision(pool, boxes, boxOnboard, ref, { apply }) {
510
509
  // Create (or reuse) the named tunnel BEFORE the droplet so its connector token
511
510
  // bakes into the cloud-init box.env. A failure here aborts provision — better
512
511
  // no box than a box that boots without its promised stable terminal.
513
- // #774: on the named-tunnel path the control plane knows EVERYTHING the browser
514
- // on-ramp needs at provision time — the stable hostname (here) + a password (we
515
- // generate it). So we bake the password into box.env (the box serves ttyd with
516
- // it) and, once the box is active, RECORD {url, password} into the GDS ourselves
517
- // (setTerminalAccess) — the builder reads them from the hall with no token ever
518
- // placed on the box and no box-side reporter (that path was the quick-tunnel
519
- // fallback and the source of the #771/#774 chicken-and-egg). ADR 0016 holds: no
520
- // builder/infra credential is funneled into the box.
512
+ // #774: on the named-tunnel path the control plane knows everything the browser
513
+ // on-ramp needs at provision time — the stable hostname (here) + a password we
514
+ // generate. So bake the password into box.env (the box serves ttyd with it) and,
515
+ // once the box is active, RECORD {url, password} into the GDS ourselves
516
+ // (setTerminalAccess): the builder reads them from the hall, with no token placed
517
+ // on the box and no box-side reporter (the quick-tunnel fallback that caused the
518
+ // #771/#774 chicken-and-egg). ADR 0016 holds — no credential funneled into a box.
521
519
  let boxEnv = '';
522
520
  let terminalHostname = null;
523
521
  let terminalCredential = null;
@@ -553,24 +551,19 @@ async function cmdProvision(pool, boxes, boxOnboard, ref, { apply }) {
553
551
  }
554
552
  }
555
553
 
556
- // Bootstrap the builder's GDS session onto the box (#818). box-source-fetch
557
- // (#600) authenticates as the BUILDER to pull the rank-scoped repo; without a
558
- // session at ~/.config/otb/gds-session.json it no-ops forever, so the repo
559
- // never clones, the devcontainer never comes up, and /builder-* skills are
560
- // absent. The session normally comes from /builder-setup — which lives in the
561
- // un-cloned repo (the chicken-and-egg). So we mint a session here and bake it
562
- // into cloud-init, exactly like the SSH keys / tunnel token / terminal credential.
554
+ // Bootstrap the builder's GDS session onto the box (#818). box-source-fetch (#600)
555
+ // authenticates as the BUILDER to pull the rank-scoped repo; with no session file
556
+ // it no-ops forever (repo never clones → no devcontainer, no /builder-* skills),
557
+ // and the session normally comes from /builder-setup, which lives in the un-cloned
558
+ // repo — the chicken-and-egg. So mint one here and bake it into cloud-init.
563
559
  //
564
- // SECURITY (#919, SR#2): this baked session is source='box' — a SCOPED, 24h
565
- // refresh-on-use credential. The token lives ON the box (readable by any on-box
566
- // process + the metadata endpoint), which is OUTSIDE the ADR 0016 trust boundary,
567
- // so it must NOT carry the builder's full authority. auth.requireBuilder denies a
568
- // 'box' session on every route except the 4 the on-box crons need (source-access,
569
- // authorized-keys, heartbeat, terminal — gated by auth.allowBoxScope). The first
570
- // source-fetch clones on boot; the repo persists in the snapshot. To do real work
571
- // FROM the box (claim/ship) the builder runs /builder-reauth (device flow), minting
572
- // a normal cli/web session — never auto-baked. user_agent='box-provision' is the
573
- // marker migration 090 keys on to retroactively scope already-baked tokens.
560
+ // SECURITY (#919, SR#2): this baked session is source='box' — SCOPED, 24h
561
+ // refresh-on-use. The token sits ON the box (any on-box process + the metadata
562
+ // endpoint can read it), OUTSIDE the ADR 0016 trust boundary, so it must not carry
563
+ // the builder's full authority: requireBuilder denies a 'box' session everywhere
564
+ // except the 4 on-box cron routes (source-access, authorized-keys, heartbeat,
565
+ // terminal — via auth.allowBoxScope). Real work FROM the box needs /builder-reauth
566
+ // (device flow); user_agent='box-provision' is the marker migration 090 keys on.
574
567
  const gdsDb = require(path.join(REPO_ROOT, 'src/bongos/db'));
575
568
  const sessionToken = crypto.randomBytes(32).toString('hex');
576
569
  await gdsDb.createSession({
@@ -6,8 +6,8 @@
6
6
  # in /var/backups/gds/ as YYYY-MM-DD-HHMMSS.sql.gz (UTC timestamp), and
7
7
  # files older than 30 days are pruned.
8
8
  #
9
- # Authentication: peer auth via the local Unix socket. The user `lars`
10
- # owns the `amazonprimea` Postgres role; pg_dump finds the socket at
9
+ # Authentication: peer auth via the local Unix socket — the invoking unix
10
+ # user owns the instance's Postgres role; pg_dump finds the socket at
11
11
  # /var/run/postgresql with no password needed. Matches the model in
12
12
  # src/db.js.
13
13
  #
@@ -36,8 +36,21 @@ set -euo pipefail
36
36
  # Destination directory for dumps. Owned by lars:lars, mode 0750.
37
37
  BACKUP_DIR="${GDS_BACKUP_DIR:-/var/backups/gds}"
38
38
 
39
- # Database to dump (default: production GDS).
40
- DB_NAME="${GDS_BACKUP_DB:-amazonprimea}"
39
+ # Database to dump: explicit GDS_BACKUP_DB wins, else the instance's own branding
40
+ # pack names it — the SAME resolver as migrate.sh / pool.js, so the backup and the
41
+ # running server can never disagree (task 1002757). Deliberately NO literal
42
+ # fallback: this script ships to every instance, and a baked-in founder default
43
+ # aimed another instance's nightly pg_dump at a database that is not its own.
44
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
46
+ DB_NAME="${GDS_BACKUP_DB:-}"
47
+ if [ -z "$DB_NAME" ]; then
48
+ DB_NAME="$(node -e "process.stdout.write(require(process.argv[1]).instanceDbName() || '')" "$REPO_ROOT/src/bongos/pool" 2>/dev/null || true)"
49
+ fi
50
+ if [ -z "$DB_NAME" ]; then
51
+ echo "db-backup: cannot determine which database to dump. Set GDS_BACKUP_DB, or give this instance a db.database in config/branding.json. Refusing to guess (task 1002757)." >&2
52
+ exit 2
53
+ fi
41
54
 
42
55
  # Retention in days. Files older than this (by mtime) are deleted at the
43
56
  # end of each successful run. 30 days = 30 daily dumps retained.
@@ -13,13 +13,13 @@
13
13
  # scripts/gds/gds-db-offsite.service + .timer). Weekly cadence.
14
14
  #
15
15
  # Authentication: S3-style credentials for the Space live OUTSIDE the repo
16
- # in /etc/amazonprimea/spaces.env (chmod 600, owned by lars), per ADR 0022.
17
- # The systemd unit injects them via EnvironmentFile; a manual run sources
18
- # the same file. Required vars:
16
+ # in /etc/<configDir>/spaces.env (chmod 600, owned by the service user), per
17
+ # ADR 0022. The systemd unit injects them via EnvironmentFile; a manual run
18
+ # sources the same file. Required vars:
19
19
  # SPACES_KEY SPACES_SECRET SPACES_BUCKET SPACES_REGION SPACES_ENDPOINT
20
20
  #
21
21
  # Manual one-shot:
22
- # /home/lars/amazonprimea/scripts/gds/db-backup-offsite.sh
22
+ # <instance-root>/scripts/gds/db-backup-offsite.sh
23
23
  #
24
24
  # Systemd:
25
25
  # sudo systemctl start gds-db-offsite.service # one-shot
@@ -32,7 +32,19 @@ set -euo pipefail
32
32
 
33
33
  # Where the local nightly dumps land (must match db-backup-nightly.sh).
34
34
  BACKUP_DIR="${GDS_BACKUP_DIR:-/var/backups/gds}"
35
- DB_NAME="${GDS_BACKUP_DB:-amazonprimea}"
35
+ # Database name: explicit GDS_BACKUP_DB wins, else the instance's branding pack —
36
+ # the same resolver + no-literal-fallback posture as db-backup-nightly.sh /
37
+ # migrate.sh (task 1002757).
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
40
+ DB_NAME="${GDS_BACKUP_DB:-}"
41
+ if [ -z "$DB_NAME" ]; then
42
+ DB_NAME="$(node -e "process.stdout.write(require(process.argv[1]).instanceDbName() || '')" "$REPO_ROOT/src/bongos/pool" 2>/dev/null || true)"
43
+ fi
44
+ if [ -z "$DB_NAME" ]; then
45
+ echo "db-backup-offsite: cannot determine which database's dumps to ship. Set GDS_BACKUP_DB, or give this instance a db.database in config/branding.json. Refusing to guess (task 1002757)." >&2
46
+ exit 2
47
+ fi
36
48
 
37
49
  # How many weekly offsite copies to retain in the Space before pruning the
38
50
  # oldest. 4 = ~1 month of weekly snapshots (Archon decision, #415 claim 338).
@@ -45,7 +57,12 @@ PREFIX="${GDS_OFFSITE_PREFIX:-gds-offsite/}"
45
57
 
46
58
  # Credentials file. Read only if the SPACES_* vars aren't already set
47
59
  # (systemd injects them via EnvironmentFile; this covers manual runs).
48
- ENV_FILE="${GDS_SPACES_ENV:-/etc/amazonprimea/spaces.env}"
60
+ # Default path derives from the instance's config dir (task 1002757 — the old
61
+ # founder-pinned /etc/<founder>/ default could never exist on another box);
62
+ # empty when the resolver is unavailable, in which case the SPACES_* vars (or
63
+ # an explicit GDS_SPACES_ENV) must carry the credentials.
64
+ CONFIG_DIR_NAME="$(node -e "process.stdout.write(require(process.argv[1]).configDirName() || '')" "$REPO_ROOT/src/instance-config" 2>/dev/null || true)"
65
+ ENV_FILE="${GDS_SPACES_ENV:-${CONFIG_DIR_NAME:+/etc/$CONFIG_DIR_NAME/spaces.env}}"
49
66
 
50
67
  # ---- load credentials -------------------------------------------------------
51
68
 
@@ -14,7 +14,7 @@
14
14
  "artist": {
15
15
  "skill": "paint",
16
16
  "label": "Art session",
17
- "directive": "This is an ARTIST task. To work it, invoke the /paint skill — that skill is the operating playbook for art sessions, and it is the only place the experience is defined. It is show-first / low-text: Claude communicates in pictures (display the generated tile, the /art gallery at sandbox-<login>.amazonprimea.com/art, and the in-world sandbox), not paragraphs about what the pipeline did. It drives the existing pixel-art pipeline (otb-tile-generate / otb-design-review / otb-feedback-capture / art-stage.js) and keeps the locked palette + rubric sacred. If no human is present (autonomous / bypass-permissions), /paint generates, rubric-gates hard, stages passing assets to the gallery, and parks the visuals for human review. Open /paint now rather than working this task the default way; the gallery is at sandbox-<login>.amazonprimea.com/art."
17
+ "directive": "This is an ARTIST task. To work it, invoke the /paint skill — that skill is the operating playbook for art sessions, and it is the only place the experience is defined. It is show-first / low-text: Claude communicates in pictures (display the generated tile, the /art gallery at sandbox-<login>.<instance-apex>/art, and the in-world sandbox), not paragraphs about what the pipeline did. It drives the existing pixel-art pipeline (otb-tile-generate / otb-design-review / otb-feedback-capture / art-stage.js) and keeps the locked palette + rubric sacred. If no human is present (autonomous / bypass-permissions), /paint generates, rubric-gates hard, stages passing assets to the gallery, and parks the visuals for human review. Open /paint now rather than working this task the default way; the gallery is at sandbox-<login>.<instance-apex>/art."
18
18
  },
19
19
  "ui": {
20
20
  "skill": "design",