@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.
- package/.bongos-core.json +121 -36
- package/.claude/skills/merge-mode/SKILL.md +3 -1
- package/.devcontainer/init-firewall.sh +41 -5
- package/clients/bongos-client/examples/hello-world.mjs +11 -2
- package/docs/copy-inventory.md +20 -20
- package/docs/copy-registry.json +22 -22
- package/modules/hall-ui/public/builders.js +0 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/audit-replay.sh +20 -3
- package/scripts/gds/box.js +79 -86
- package/scripts/gds/db-backup-nightly.sh +17 -4
- package/scripts/gds/db-backup-offsite.sh +23 -6
- package/scripts/gds/discipline-modes.json +1 -1
- package/scripts/gds/doc-cli-guard.js +34 -2
- package/scripts/gds/fitness.js +8 -4
- package/scripts/gds/gds-db-backup.service +15 -4
- package/scripts/gds/gds-db-backup.timer +12 -1
- package/scripts/gds/gds-db-offsite.service +16 -5
- package/scripts/gds/gds-db-offsite.timer +12 -1
- package/scripts/gds/instance-apex.js +28 -0
- package/scripts/gds/newcomer-floor.templates.json +2 -2
- package/scripts/gds/provision.js +7 -3
- package/scripts/gds/ship-deploy-target.js +388 -0
- package/scripts/gds/ship-finish.js +377 -0
- package/scripts/gds/ship-flow.js +814 -0
- package/scripts/gds/ship-git.js +100 -0
- package/scripts/gds/ship-grade-steps.js +376 -0
- package/scripts/gds/ship-grade.js +319 -0
- package/scripts/gds/ship-io.js +100 -0
- package/scripts/gds/ship-land.js +539 -0
- package/scripts/gds/ship-memory-sync.js +192 -0
- package/scripts/gds/ship-merge.js +297 -0
- package/scripts/gds/ship-preflight-steps.js +323 -0
- package/scripts/gds/ship-regen.js +286 -0
- package/scripts/gds/ship-resume.js +151 -0
- package/scripts/gds/ship-session-upload.js +216 -0
- package/scripts/gds/ship-state.js +73 -0
- package/scripts/gds/ship-surprise.js +84 -0
- package/scripts/gds/ship.js +105 -4195
- package/scripts/status-probe.mjs +30 -2
- package/src/module-api.js +3 -1
- package/tests/helpers.mjs +13 -0
- package/tests/provision.mjs +7 -1
- package/tests/regrade_eligibility.mjs +43 -0
- package/tests/ship_assurance.mjs +4 -2
- package/tests/ship_publish_error.mjs +43 -0
package/docs/copy-registry.json
CHANGED
|
@@ -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:
|
|
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:
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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":
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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"
|
package/scripts/gds/box.js
CHANGED
|
@@ -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
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
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
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
zone: process.env.CLOUDFLARE_ZONE ||
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
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
|
|
162
|
-
//
|
|
163
|
-
//
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
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
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
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
|
|
514
|
-
// on-ramp needs at provision time — the stable hostname (here) + a password
|
|
515
|
-
// generate
|
|
516
|
-
//
|
|
517
|
-
// (setTerminalAccess)
|
|
518
|
-
//
|
|
519
|
-
//
|
|
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
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
//
|
|
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' —
|
|
565
|
-
// refresh-on-use
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
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
|
|
10
|
-
# owns the
|
|
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
|
|
40
|
-
|
|
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
|
|
17
|
-
# The systemd unit injects them via EnvironmentFile; a manual run
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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",
|