@bongos/core 1.19.1032 → 1.19.1034
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 +322 -117
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-audit/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +5 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/0175-the-board-room-full-idea-ratification.md +3 -2
- package/docs/adr/0178-the-copy-desk-flag-and-queue.md +3 -3
- package/docs/adr/0188-patrol-runs-on-ci-not-the-server.md +2 -2
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +1 -1
- package/docs/adr/0233-a-copy-proposal-is-a-task-carrying-a-patch.md +1 -1
- package/docs/adr/0241-the-artist-gate-is-a-per-project-deploy-gate-that-reads-a-state.md +11 -3
- package/docs/adr/README.md +4 -4
- package/docs/api/openapi.json +1 -1
- package/docs/architecture.md +4 -3
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/file-map.md +4 -3
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/autobongos-windows-host.md +3 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +3 -1
- package/docs/recipes/redteam-patrol.md +2 -2
- package/docs/recipes/upgrading-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -0
- package/modules/autonomy/gauge.js +57 -9
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/hall-ui/records/builders-roster.md +10 -0
- package/modules/hall-ui/records/catalog.md +10 -0
- package/modules/hall-ui/records/collaboration.md +14 -0
- package/modules/hall-ui/records/goal-dependency-forest.md +11 -0
- package/modules/hall-ui/records/oversight-family.md +19 -0
- package/modules/hall-ui/records/panel-family.md +19 -0
- package/modules/hall-ui/records/reading-rooms.md +12 -0
- package/modules/hall-ui/records/record-family.md +13 -0
- package/modules/hall-ui/records/reference-room.md +9 -0
- package/modules/hall-ui/records/roster-and-ledger.md +9 -0
- package/modules/hall-ui/records/sky.md +23 -0
- package/modules/lifecycle/dependency-advisory.js +2 -1
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-preflight.js +91 -0
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +7481 -0
- package/scripts/gds/address-park-sweep.sh +3 -1
- package/scripts/gds/autobongos-run.js +91 -3
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/baseline-staleness.js +1 -1
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/db-backup-nightly.sh +9 -4
- package/scripts/gds/db-backup-offsite.sh +4 -2
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/package-core.js +72 -1
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +282 -80
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/redteam-patrol.js +4 -4
- package/scripts/gds/release-notes.js +271 -0
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/seed-board-room-tasks.js +1 -1
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-flow.js +5 -3
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +241 -16
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +86 -11
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/stray-unit-sweep.sh +3 -1
- package/scripts/gds/upgrade-migrate.js +67 -0
- package/scripts/gds/upgrade.js +9 -7
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/adr_0162_citation.mjs +184 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/autobongos_loop.mjs +114 -0
- package/tests/autonomy_gauge.mjs +56 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +329 -5
- package/tests/credential_presence.mjs +153 -0
- package/tests/db_backup_nightly.mjs +21 -3
- package/tests/db_backup_offsite.mjs +81 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/hall_records.mjs +85 -0
- package/tests/helpers.mjs +25 -0
- package/tests/instance_db_migrated_on_code_move.mjs +8 -1
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/plain_cards.mjs +1 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/redteam_patrol.mjs +1 -1
- package/tests/release_notes.mjs +327 -0
- package/tests/shell_stat_portability.mjs +49 -0
- package/tests/ship_land_conflict.mjs +181 -0
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +226 -3
- package/tests/start_env_guard.mjs +16 -0
- package/tests/sweep_app_account.mjs +15 -2
- package/tests/upgrade.mjs +20 -1
- package/tests/upgrade_migrate.mjs +195 -0
|
@@ -31,7 +31,9 @@ DB="${PGDATABASE:-cloudbongos}"
|
|
|
31
31
|
CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
32
32
|
case "$CORE_DIR" in */node_modules/@bongos/core) INSTANCE_DIR="${INSTANCE_DIR:-${CORE_DIR%/node_modules/@bongos/core}}" ;; esac
|
|
33
33
|
[ -n "${INSTANCE_DIR:-}" ] || { echo "set INSTANCE_DIR to the control plane's instance checkout — this sweep is not running from one"; exit 1; }
|
|
34
|
-
|
|
34
|
+
# Owner by name: GNU stat spells it `-c %U`, BSD/macOS stat `-f %Su`, and each refuses the
|
|
35
|
+
# other's flag, so GNU is asked first and BSD answers when it refuses (task 1004170).
|
|
36
|
+
APP_USER="${PROVISION_APP_USER:-$(stat -c %U "$INSTANCE_DIR" 2>/dev/null || stat -f %Su "$INSTANCE_DIR" 2>/dev/null)}"
|
|
35
37
|
if [ "$APP_USER" = root ] || ! [[ "$APP_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
|
|
36
38
|
echo "cannot use '$APP_USER' as the app account — set PROVISION_APP_USER to the unprivileged account that owns $INSTANCE_DIR"; exit 1
|
|
37
39
|
fi
|
|
@@ -49,6 +49,7 @@ const fence = require('../../modules/autonomy/fence.js');
|
|
|
49
49
|
// where it would not be inside modules/ — which is exactly why fence.js takes the
|
|
50
50
|
// hits as an argument instead of matching them itself.
|
|
51
51
|
const { matchProtected } = require('../../src/bongos/permission-path-check');
|
|
52
|
+
const { resolveEnv, envPrefix, LEGACY_ENV_PREFIXES } = require('../../src/instance-config');
|
|
52
53
|
const cadence = require('../../modules/autonomy/cadence.js');
|
|
53
54
|
|
|
54
55
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -94,6 +95,56 @@ function logPath() {
|
|
|
94
95
|
try { return require('../../src/instance-config.js').configPath('autobongos-runs.jsonl'); }
|
|
95
96
|
catch (_) { return path.join(REPO_ROOT, 'autobongos-runs.jsonl'); }
|
|
96
97
|
}
|
|
98
|
+
// countWorkedSince — how many tasks this runner has worked since an epoch-second
|
|
99
|
+
// mark. Reads the run log, because that is the only record that survives a
|
|
100
|
+
// restart, and the cap it feeds has to hold across one (a runner that forgets
|
|
101
|
+
// its count on every crash has no cap at all).
|
|
102
|
+
//
|
|
103
|
+
// BOUNDED ON BOTH AXES, because this runs every iteration of a loop that never
|
|
104
|
+
// exits and the log only grows. It reads the TAIL rather than the file, and it
|
|
105
|
+
// scans BACKWARDS and stops at the first row older than the mark — the rows that
|
|
106
|
+
// can possibly count are the newest ones, so the common case touches a handful
|
|
107
|
+
// of lines whatever the log's size.
|
|
108
|
+
//
|
|
109
|
+
// A torn or missing log counts as zero rather than refusing: the fence and the
|
|
110
|
+
// gauge are the gates, this is a bound on top of them, and a bound that bricks
|
|
111
|
+
// the runner when its own log is unreadable is worse than one that occasionally
|
|
112
|
+
// allows an extra task.
|
|
113
|
+
const WORKED_SCAN_TAIL_BYTES = 1024 * 1024;
|
|
114
|
+
const NEWLINE = String.fromCharCode(10);
|
|
115
|
+
|
|
116
|
+
function countWorkedSince(sinceEpochS) {
|
|
117
|
+
if (!Number.isFinite(sinceEpochS) || sinceEpochS <= 0) return 0;
|
|
118
|
+
let text;
|
|
119
|
+
try {
|
|
120
|
+
const p = logPath();
|
|
121
|
+
const { size } = fs.statSync(p);
|
|
122
|
+
const from = Math.max(0, size - WORKED_SCAN_TAIL_BYTES);
|
|
123
|
+
const fd = fs.openSync(p, 'r');
|
|
124
|
+
try {
|
|
125
|
+
const buf = Buffer.alloc(Math.min(size, WORKED_SCAN_TAIL_BYTES));
|
|
126
|
+
fs.readSync(fd, buf, 0, buf.length, from);
|
|
127
|
+
text = buf.toString('utf8');
|
|
128
|
+
} finally { fs.closeSync(fd); }
|
|
129
|
+
// A tail read can land mid-line; drop the first partial one.
|
|
130
|
+
if (from > 0) text = text.slice(text.indexOf(NEWLINE) + 1);
|
|
131
|
+
} catch (_) { return 0; }
|
|
132
|
+
|
|
133
|
+
const lines = text.split(NEWLINE);
|
|
134
|
+
let n = 0;
|
|
135
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
136
|
+
const line = lines[i];
|
|
137
|
+
if (!line.trim()) continue;
|
|
138
|
+
let row;
|
|
139
|
+
try { row = JSON.parse(line); } catch (_) { continue; } // a torn line costs one row, not the count
|
|
140
|
+
const at = Date.parse(row.at);
|
|
141
|
+
if (!Number.isFinite(at)) continue;
|
|
142
|
+
if (at / 1000 < sinceEpochS) break; // the log is append-ordered: nothing older can count
|
|
143
|
+
if (row.event === 'worked') n += 1;
|
|
144
|
+
}
|
|
145
|
+
return n;
|
|
146
|
+
}
|
|
147
|
+
|
|
97
148
|
function record(entry) {
|
|
98
149
|
const row = { at: new Date().toISOString(), ...entry };
|
|
99
150
|
try {
|
|
@@ -156,11 +207,18 @@ async function pickTask(goals, deps = {}) {
|
|
|
156
207
|
// own required vars), to reach the instance (BONGOS_API_BASE), and to be told it
|
|
157
208
|
// is a subagent. It does NOT need a git push credential — the road to main is the
|
|
158
209
|
// server-mediated pipeline (ADR 0307 §6) — so none is passed.
|
|
210
|
+
function claudeBinEnvNames() {
|
|
211
|
+
return [...new Set([envPrefix(), ...LEGACY_ENV_PREFIXES].map((p) => `${p}_CLAUDE_BIN`)), 'CLAUDE_BIN'];
|
|
212
|
+
}
|
|
213
|
+
|
|
159
214
|
const WORKER_ENV_ALLOW = [
|
|
160
215
|
'PATH', 'HOME', 'SHELL', 'LANG', 'LC_ALL', 'TZ', 'TMPDIR', 'USER', 'LOGNAME',
|
|
161
216
|
'SystemRoot', 'windir', 'COMSPEC', 'PATHEXT', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'TEMP', 'TMP',
|
|
162
217
|
'BONGOS_API_BASE', 'CLOUDBONGOS_API_BASE',
|
|
163
|
-
|
|
218
|
+
// Every spelling resolveWorkerBin below can read (task 1001441): the branded
|
|
219
|
+
// <PREFIX>_CLAUDE_BIN, the legacy ones, and the unprefixed CLAUDE_BIN. A name
|
|
220
|
+
// missing here would be set on the host and silently absent in the worker.
|
|
221
|
+
...claudeBinEnvNames(),
|
|
164
222
|
'XDG_CONFIG_HOME',
|
|
165
223
|
// THE WORKER'S OWN CREDENTIAL, and it has to be here (task 1003907, found by
|
|
166
224
|
// running it). On a desktop, the claude CLI authenticates from the credential
|
|
@@ -207,7 +265,7 @@ const MAX_WORKER_OUTPUT = 8 * 1024 * 1024; // a misbehaving worker must not gr
|
|
|
207
265
|
// laid-out install should still run, and a corrupted schema surfaces as a failed
|
|
208
266
|
// worker rather than as a refusal to start at all.
|
|
209
267
|
function resolveWorkerBin(env = process.env, platform = process.platform) {
|
|
210
|
-
const override = env
|
|
268
|
+
const override = resolveEnv('CLAUDE_BIN', { env }) || env.CLAUDE_BIN;
|
|
211
269
|
if (override) return { bin: override, shell: platform === 'win32' && /\.(cmd|bat)$/i.test(override) };
|
|
212
270
|
if (platform !== 'win32') return { bin: 'claude', shell: false };
|
|
213
271
|
for (const dir of String(env.PATH || env.Path || '').split(path.delimiter)) {
|
|
@@ -342,6 +400,36 @@ async function iteration(opts, deps = {}) {
|
|
|
342
400
|
return log({ event: 'hold', go: false, unknown: !!g.decision.unknown, reason: g.decision.reason, sleep_until: g.decision.sleepUntil ?? null });
|
|
343
401
|
}
|
|
344
402
|
|
|
403
|
+
// 1b. A DERIVED gauge buys a BOUNDED amount of work (task 1004178).
|
|
404
|
+
//
|
|
405
|
+
// `derived` means the last real reading's window had already reset, so the
|
|
406
|
+
// baseline is a retired observation rather than a measurement. That is sound
|
|
407
|
+
// for the FIVE-hour window, which turns over during a night. It does nothing
|
|
408
|
+
// for the SEVEN-day one, which does not — so the weekly pace check, the thing
|
|
409
|
+
// the module's notes call what actually binds a heavy week, is simply absent
|
|
410
|
+
// on a derived reading. A stale weekly figure cannot substitute: usage only
|
|
411
|
+
// grows, so an under-reported value makes the check more permissive, and that
|
|
412
|
+
// is the wrong direction to be wrong in.
|
|
413
|
+
//
|
|
414
|
+
// So the bound is a task count, not a percentage: on one derived window, do
|
|
415
|
+
// AUTOBONGOS_DERIVED_TASK_CAP tasks and then hold until a real reading exists.
|
|
416
|
+
// A terminal session writes one; the owner opening a terminal in the morning
|
|
417
|
+
// is what lifts it. Default 2 — each worker is wall-clock bounded at 90
|
|
418
|
+
// minutes, so two of them is well inside a five-hour window even at worst, and
|
|
419
|
+
// the cap only has to be small enough that being wrong is survivable.
|
|
420
|
+
if (g.decision.derived) {
|
|
421
|
+
const cap = Number(process.env.AUTOBONGOS_DERIVED_TASK_CAP ?? 2);
|
|
422
|
+
const since = Number(g.decision.derivedSince) || 0;
|
|
423
|
+
const done = countWorkedSince(since);
|
|
424
|
+
if (Number.isFinite(cap) && cap >= 0 && done >= cap) {
|
|
425
|
+
return log({
|
|
426
|
+
event: 'hold', go: false, unknown: false, derived: true, worked_since_derived: done,
|
|
427
|
+
reason: `derived gauge: ${done} task(s) already done since the window reset and the cap is ${cap} — holding for a real reading (a terminal session writes one)`,
|
|
428
|
+
sleep_until: null,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
345
433
|
// 2. Pick — from the ALLOWLISTED goals, never from what the command line asked
|
|
346
434
|
// for. gate.goals is already the intersection.
|
|
347
435
|
const picked = await pick(gate.goals, deps.pickDeps || {});
|
|
@@ -798,7 +886,7 @@ if (require.main === module) {
|
|
|
798
886
|
|
|
799
887
|
module.exports = {
|
|
800
888
|
readArgv, pickTask, iteration, record, logPath, spawnWorker, workerEnv, WORKER_ENV_ALLOW, resolveWorkerBin,
|
|
801
|
-
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX,
|
|
889
|
+
worktreeName, readFence, reconcileLeftoverClaims, pollSignals, waitOrJump, forever, CLAIM_PREFIX, countWorkedSince,
|
|
802
890
|
heartbeatPath, writeHeartbeat, readHeartbeat, pidAlive, anotherRunnerIsAlive, HEARTBEAT_STALE_MS,
|
|
803
891
|
isRunnerTree, RUNNER_TREE_RE, postHeartbeat, RUNNER_STARTED_AT,
|
|
804
892
|
};
|
|
@@ -12,23 +12,28 @@
|
|
|
12
12
|
//
|
|
13
13
|
// If no path is given, the script searches for the ledger in the
|
|
14
14
|
// expected locations (current art/iterations/, sibling worktrees, or
|
|
15
|
-
//
|
|
15
|
+
// $<PREFIX>_COST_LEDGER env var — BONGOS_COST_LEDGER on a vanilla instance, the
|
|
16
|
+
// legacy OTB_COST_LEDGER still read).
|
|
16
17
|
|
|
17
18
|
const fs = require('node:fs');
|
|
18
19
|
const path = require('node:path');
|
|
19
20
|
const { cliClient, requireSession, fmtUsd } = require('./cli-lib');
|
|
21
|
+
const { resolveEnv, envPrefix } = require('../../src/instance-config');
|
|
20
22
|
// Cost-attribution audit carved into modules/economy/cost-classify.js (BV1.R77 /
|
|
21
23
|
// ADR 0093 §1-2). A CLI may require a module directly (the memory/grading-carve
|
|
22
24
|
// precedent — grade-smoke.js requires modules/grading/grader).
|
|
23
25
|
const { auditCostAttribution, formatAuditReport } = require('../../modules/economy/cost-classify');
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
// The places a ledger is looked for, env override first. A function so the test can
|
|
28
|
+
// hand it an env; resolved once below for the CLI.
|
|
29
|
+
const knownLedgerPaths = (env = process.env) => [
|
|
30
|
+
resolveEnv('COST_LEDGER', { env }),
|
|
27
31
|
path.resolve(__dirname, '../../art/iterations/cost_ledger.jsonl'),
|
|
28
32
|
// Two historical absolute-path fallbacks (under the old Desktop folder name)
|
|
29
33
|
// were dropped after the project-folder rename. Pass the ledger path as a CLI
|
|
30
|
-
// arg or set
|
|
34
|
+
// arg or set <PREFIX>_COST_LEDGER to point at any non-default location.
|
|
31
35
|
];
|
|
36
|
+
const KNOWN_LEDGER_PATHS = knownLedgerPaths();
|
|
32
37
|
|
|
33
38
|
function findLedger(argPath) {
|
|
34
39
|
const candidates = argPath ? [argPath, ...KNOWN_LEDGER_PATHS] : KNOWN_LEDGER_PATHS;
|
|
@@ -106,7 +111,7 @@ async function main() {
|
|
|
106
111
|
|
|
107
112
|
const ledgerPath = findLedger(argPath);
|
|
108
113
|
if (!ledgerPath) {
|
|
109
|
-
console.error(
|
|
114
|
+
console.error(`No cost_ledger.jsonl found. Pass a path or set ${envPrefix()}_COST_LEDGER.`);
|
|
110
115
|
console.error('Searched:');
|
|
111
116
|
for (const p of KNOWN_LEDGER_PATHS) if (p) console.error(` - ${p}`);
|
|
112
117
|
process.exit(2);
|
|
@@ -159,7 +164,13 @@ async function main() {
|
|
|
159
164
|
process.exit(failed > 0 ? 1 : 0);
|
|
160
165
|
}
|
|
161
166
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
// Run only when invoked directly: requiring this file (the test does, for
|
|
168
|
+
// knownLedgerPaths) must never start a backfill.
|
|
169
|
+
if (require.main === module) {
|
|
170
|
+
main().catch((err) => {
|
|
171
|
+
console.error('backfill-cost.js failed:', err);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { knownLedgerPaths };
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
// anybody's diff. Hard-failing a PR because ambient debt turned eight weeks old
|
|
41
41
|
// punishes whoever pushed next and gets the gate disabled. The audit asked for
|
|
42
42
|
// "the repeatable, SCHEDULED version" of its own manual review, and the patrol
|
|
43
|
-
// is that: history-wide, time-based, filed for adjudication (ADR
|
|
43
|
+
// is that: history-wide, time-based, filed for adjudication (ADR 0158 §2).
|
|
44
44
|
//
|
|
45
45
|
// AGE COMES FROM GIT, NOT FROM A STORED TIMESTAMP. A `since:` field in the JSON
|
|
46
46
|
// would be faster and hermetic, and it would also be the thing that silently
|
package/scripts/gds/claim.js
CHANGED
|
@@ -225,8 +225,8 @@ function captureHeadSha() {
|
|
|
225
225
|
// origin/main. The completion cards + skills are CLI-script features that ride
|
|
226
226
|
// the LOCAL checkout, not the deployed server — a worktree branched from a stale
|
|
227
227
|
// main silently lacks them (the root cause behind the missing cards in session
|
|
228
|
-
// 1c1e12d7: claimed/shipped task 1274 on old scripts). Mirrors ship
|
|
229
|
-
//
|
|
228
|
+
// 1c1e12d7: claimed/shipped task 1274 on old scripts). Mirrors the ship's
|
|
229
|
+
// TOOLING_PATHSPECS preflight (ship-preflight-steps.js), but as a NUDGE — claiming on old scripts is fine, you
|
|
230
230
|
// just miss features. Strictly best-effort + fail-open (never break/slow a claim).
|
|
231
231
|
const STALE_TOOLING_DIRS = ['scripts/gds', 'src/bongos', '.claude/skills'];
|
|
232
232
|
function warnIfToolingStale() {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/credential-presence.js — which credentials does this HOST hold? Names and
|
|
3
|
+
// presence only, never a value (task 1002880, from idea 1000630).
|
|
4
|
+
//
|
|
5
|
+
// Run ON THE HOST, as the user that can read the instance's env files (usually via sudo):
|
|
6
|
+
// node scripts/gds/credential-presence.js # reads /etc/<configDir>/*.env
|
|
7
|
+
// node scripts/gds/credential-presence.js --etc /etc/example --json
|
|
8
|
+
//
|
|
9
|
+
// WHY. The owner was asked to create GitHub credentials at least three times, and each
|
|
10
|
+
// time the credential already existed — nothing could answer "what does this instance
|
|
11
|
+
// need, what is set, what is missing". docs/credentials.md is the written half (what each
|
|
12
|
+
// credential is for, where it lives, who may hold it); this is the one-command half, run
|
|
13
|
+
// where the values actually live. doctor.js cannot do it: it runs on a builder's laptop,
|
|
14
|
+
// which never sees the host's env files, and that is by design.
|
|
15
|
+
//
|
|
16
|
+
// THE ONE RULE: A VALUE NEVER LEAVES THE FILE. The report says set / empty / missing and
|
|
17
|
+
// the NAME of the variable that satisfied each row — never the value, never a prefix,
|
|
18
|
+
// never a length. tests/credential_presence.mjs asserts no fragment of any value reaches
|
|
19
|
+
// the output. If this is ever surfaced over HTTP it must be Archon-gated, and it still
|
|
20
|
+
// returns names only.
|
|
21
|
+
//
|
|
22
|
+
// Deeper than presence: `node scripts/gds/publish-credential-check.js` (task 1002733)
|
|
23
|
+
// probes whether the publish credential can actually read the Actions API. This file
|
|
24
|
+
// does not call GitHub — presence is the question it answers, offline.
|
|
25
|
+
//
|
|
26
|
+
// Exit: 0 = every REQUIRED row is satisfied · 1 = a required row is missing ·
|
|
27
|
+
// 2 = an env file this host should have could not be read (the answer is unknown).
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const { resolveEnv, envPrefix, configDirName, configPath, LEGACY_ENV_PREFIXES } = require('../../src/instance-config');
|
|
34
|
+
const { arg, hasFlag, cliExit } = require('./cli-lib');
|
|
35
|
+
|
|
36
|
+
// ---- the inventory -------------------------------------------------------------------
|
|
37
|
+
//
|
|
38
|
+
// One row per credential this instance's HOST may hold. `names` are the env var(s) that
|
|
39
|
+
// must ALL be non-empty for the row to count as set; `prefixed` names resolve through the
|
|
40
|
+
// instance's own env prefix (and its legacy spellings) the way the server reads them.
|
|
41
|
+
// Rows sharing a `group` are alternatives: the group is satisfied when any member is.
|
|
42
|
+
// `file` is the env file the value lives in on a control-plane host:
|
|
43
|
+
// web.env — the web tier (the cloudbongos.service EnvironmentFile)
|
|
44
|
+
// box.env — the control-plane runners; deliberately unreadable by the web tier
|
|
45
|
+
// (InaccessiblePaths=, task 1004176 / ADR 0327 §5)
|
|
46
|
+
// spaces.env — the offsite-backup timer
|
|
47
|
+
// `need`: required (the instance is broken without it) · recommended (a named feature
|
|
48
|
+
// is off without it) · optional.
|
|
49
|
+
//
|
|
50
|
+
// Every name here is documented in docs/credentials.md; the test fails if either side
|
|
51
|
+
// gains a name the other lacks. Non-host credentials (GitHub Actions secrets, a
|
|
52
|
+
// builder's own machine) are in the doc only — no host sweep can see them.
|
|
53
|
+
const CREDENTIALS = [
|
|
54
|
+
{
|
|
55
|
+
id: 'github-sign-in', group: 'sign-in', file: 'web.env', need: 'required',
|
|
56
|
+
names: ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'], prefixed: true,
|
|
57
|
+
purpose: 'GitHub OAuth app that signs builders in (src/bongos/auth-config.js)',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'hub-federation', group: 'sign-in', file: 'web.env', need: 'required',
|
|
61
|
+
names: ['HUB_ORIGIN', 'HUB_CLIENT_ID', 'HUB_CLIENT_SECRET', 'HUB_PUBLIC_KEY'], prefixed: true,
|
|
62
|
+
purpose: 'sign-in federated through the Cloud Bongos hub instead of an own GitHub app (ADR 0141 §4)',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'publish-app', group: 'publish', file: 'web.env', need: 'required',
|
|
66
|
+
names: ['GITHUB_APP_ID', 'GITHUB_APP_PRIVATE_KEY'],
|
|
67
|
+
purpose: 'GitHub App the server publishes builder branches and PRs with (modules/lifecycle/github-push.js); preferred',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'publish-pat', group: 'publish', file: 'web.env', need: 'required',
|
|
71
|
+
names: ['GITHUB_PUSH_TOKEN'],
|
|
72
|
+
purpose: 'fine-grained PAT fallback for the same publish path, kept for cutover',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'repo-pin', file: 'web.env', need: 'required',
|
|
76
|
+
names: ['GITHUB_OWNER', 'GITHUB_REPO'], prefixed: true, notSecret: true,
|
|
77
|
+
purpose: "which GitHub repo this instance is — mandatory where the core runs from node_modules, which has no git remote to read (src/bongos/repo-info.js)",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'secret-box-key', file: 'web.env', need: 'required',
|
|
81
|
+
names: ['BUILDER_SECRET_KEY'], keyFile: 'builder-secret.key',
|
|
82
|
+
purpose: "master key encrypting builders' stored AI keys and GitHub tokens (src/bongos/secret-box.js); the key file in the service user's config dir also counts",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'database-password', file: 'web.env', need: 'optional',
|
|
86
|
+
names: ['PGPASSWORD'],
|
|
87
|
+
purpose: "the instance's own Postgres role (src/bongos/pool.js); absent means local peer auth",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'database-url', file: 'web.env', need: 'optional',
|
|
91
|
+
names: ['DATABASE_URL'],
|
|
92
|
+
purpose: 'a full Postgres connection string, when the database is not local',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'discord-bot', file: 'web.env', need: 'recommended',
|
|
96
|
+
names: ['DISCORD_BOT_TOKEN'],
|
|
97
|
+
purpose: 'the Discord bot (modules/discord); also readable from discord-bot.json in the config dir',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'discord-oauth', file: 'web.env', need: 'optional',
|
|
101
|
+
names: ['DISCORD_CLIENT_ID', 'DISCORD_CLIENT_SECRET'],
|
|
102
|
+
purpose: 'linking a Discord account at sign-in (src/bongos/auth-discord.js)',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'discord-ship-news', file: 'web.env', need: 'optional',
|
|
106
|
+
names: ['DISCORD_SHIP_NEWS_WEBHOOK'],
|
|
107
|
+
purpose: '#ship-news posts (scripts/discord/lib.js); discord-webhooks.json in the config dir also works',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'prelaunch-password', file: 'web.env', need: 'optional',
|
|
111
|
+
names: ['PRELAUNCH_PASSWORD'], legacyNames: ['STEALTH_PASSWORD'],
|
|
112
|
+
purpose: 'the pre-launch site password (src/bongos/prelaunch-gate.js); STEALTH_PASSWORD is the deprecated alias',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'metrics-token', file: 'web.env', need: 'optional',
|
|
116
|
+
names: ['METRICS_TOKEN'],
|
|
117
|
+
purpose: 'bearer token for a remote Prometheus scrape of /metrics (src/bongos/metrics.js)',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'hub-signing-key', file: 'web.env', need: 'optional',
|
|
121
|
+
names: ['HUB_SIGNING_KEY'], prefixed: true,
|
|
122
|
+
purpose: 'the HUB only: signs the assertions federated instances verify (modules/platform-identity/hub-keys.js)',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'digitalocean', file: 'box.env', need: 'recommended',
|
|
126
|
+
names: ['DO_API_TOKEN'],
|
|
127
|
+
purpose: 'creates and destroys project servers (scripts/gds/do-api.js, provisioning); control plane only',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'cloudflare', file: 'box.env', need: 'recommended',
|
|
131
|
+
names: ['CLOUDFLARE_API_TOKEN'],
|
|
132
|
+
purpose: 'DNS records for project addresses (scripts/gds/provision-net.js); control plane only',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'offsite-backup', file: 'spaces.env', need: 'optional',
|
|
136
|
+
names: ['SPACES_KEY', 'SPACES_SECRET', 'SPACES_BUCKET'],
|
|
137
|
+
// The same file also carries the region, the endpoint and the age RECIPIENT the dumps
|
|
138
|
+
// are encrypted to. None is a secret (a recipient is a public key), so none is swept.
|
|
139
|
+
purpose: 'the weekly offsite copy of the database dumps (scripts/gds/db-backup-offsite.sh)',
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const ENV_FILES = ['web.env', 'box.env', 'spaces.env'];
|
|
144
|
+
|
|
145
|
+
// ---- pure core ------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
// parseEnvFile(text) → { NAME: value } for a systemd EnvironmentFile: KEY=VALUE lines,
|
|
148
|
+
// `#` comments, blank lines, optional surrounding quotes. Values are held only long enough
|
|
149
|
+
// to test for emptiness; nothing returned by the report functions below contains one.
|
|
150
|
+
function parseEnvFile(text) {
|
|
151
|
+
const out = {};
|
|
152
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
153
|
+
const line = raw.trim();
|
|
154
|
+
if (!line || line.startsWith('#')) continue;
|
|
155
|
+
const eq = line.indexOf('=');
|
|
156
|
+
if (eq <= 0) continue;
|
|
157
|
+
const key = line.slice(0, eq).trim().replace(/^export\s+/, '');
|
|
158
|
+
let value = line.slice(eq + 1).trim();
|
|
159
|
+
if (value.length >= 2 && (value[0] === '"' || value[0] === "'") && value[value.length - 1] === value[0]) {
|
|
160
|
+
value = value.slice(1, -1);
|
|
161
|
+
}
|
|
162
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) out[key] = value;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Every spelling a name answers to, in the order the server tries them.
|
|
168
|
+
function spellingsOf(name, row, prefix) {
|
|
169
|
+
if (row.prefixed) return [...new Set([prefix, ...LEGACY_ENV_PREFIXES])].map((p) => `${p}_${name}`);
|
|
170
|
+
return [name, ...(row.legacyNames || [])];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// checkRow(row, env, { prefix, keyFileExists }) → { state, satisfiedBy, missing }
|
|
174
|
+
// state: 'set' | 'empty' | 'missing'. satisfiedBy / missing are variable NAMES.
|
|
175
|
+
function checkRow(row, env, { prefix = envPrefix(), keyFileExists = false } = {}) {
|
|
176
|
+
const satisfiedBy = [];
|
|
177
|
+
const missing = [];
|
|
178
|
+
let anyEmpty = false;
|
|
179
|
+
for (const name of row.names) {
|
|
180
|
+
const spellings = spellingsOf(name, row, prefix);
|
|
181
|
+
const value = row.prefixed ? resolveEnv(name, { env, prefix, warn: () => {} }) : spellings.map((s) => env[s]).find((v) => v);
|
|
182
|
+
if (value) {
|
|
183
|
+
satisfiedBy.push(spellings.find((s) => env[s] === value) || spellings[0]);
|
|
184
|
+
} else {
|
|
185
|
+
if (spellings.some((s) => s in env)) anyEmpty = true;
|
|
186
|
+
missing.push(spellings[0]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (missing.length && row.keyFile && keyFileExists) {
|
|
190
|
+
return { state: 'set', satisfiedBy: [`${row.keyFile} (key file)`], missing: [] };
|
|
191
|
+
}
|
|
192
|
+
const state = missing.length === 0 ? 'set' : anyEmpty ? 'empty' : 'missing';
|
|
193
|
+
return { state, satisfiedBy, missing };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// sweep({ files, prefix, keyFileExists }) → { rows, groups, unreadable, ok }
|
|
197
|
+
// files: { 'web.env': { env } | { error } } — the parsed files, or why one could not be read.
|
|
198
|
+
function sweep({ files, prefix = envPrefix(), keyFileExists = false, credentials = CREDENTIALS }) {
|
|
199
|
+
const unreadable = Object.entries(files).filter(([, f]) => f.error).map(([name, f]) => ({ file: name, error: f.error }));
|
|
200
|
+
const rows = credentials.map((row) => {
|
|
201
|
+
const f = files[row.file];
|
|
202
|
+
if (!f || f.error) return { ...pick(row), state: 'unknown', satisfiedBy: [], missing: [] };
|
|
203
|
+
return { ...pick(row), ...checkRow(row, f.env, { prefix, keyFileExists }) };
|
|
204
|
+
});
|
|
205
|
+
// A group is satisfied when any member is; its members are not individually required.
|
|
206
|
+
const groups = {};
|
|
207
|
+
for (const r of rows) {
|
|
208
|
+
if (!r.group) continue;
|
|
209
|
+
groups[r.group] = groups[r.group] || { satisfied: false, unknown: false, members: [] };
|
|
210
|
+
groups[r.group].members.push(r.id);
|
|
211
|
+
if (r.state === 'set') groups[r.group].satisfied = true;
|
|
212
|
+
if (r.state === 'unknown') groups[r.group].unknown = true;
|
|
213
|
+
}
|
|
214
|
+
const requiredMissing = rows.filter((r) => r.need === 'required' && r.state !== 'set' && r.state !== 'unknown'
|
|
215
|
+
&& !(r.group && groups[r.group].satisfied));
|
|
216
|
+
const ok = requiredMissing.length === 0 && unreadable.length === 0;
|
|
217
|
+
return { rows, groups, unreadable, requiredMissing: requiredMissing.map((r) => r.id), ok };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function pick(row) {
|
|
221
|
+
return { id: row.id, file: row.file, need: row.need, group: row.group || null, purpose: row.purpose, notSecret: !!row.notSecret };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// format(result) → the human report. Built only from ids, names, states and the fixed
|
|
225
|
+
// purpose text — never from a parsed value.
|
|
226
|
+
function format(result, { etc } = {}) {
|
|
227
|
+
const mark = { set: '✓', empty: '!', missing: '✗', unknown: '?' };
|
|
228
|
+
const lines = [`Credential presence — names only, never values (${etc || 'env files'})`, ''];
|
|
229
|
+
for (const file of ENV_FILES) {
|
|
230
|
+
const rows = result.rows.filter((r) => r.file === file);
|
|
231
|
+
if (!rows.length) continue;
|
|
232
|
+
const bad = result.unreadable.find((u) => u.file === file);
|
|
233
|
+
lines.push(` ${file}${bad ? ` — could not read: ${bad.error}` : ''}`);
|
|
234
|
+
for (const r of rows) {
|
|
235
|
+
const grp = r.group ? result.groups[r.group] : null;
|
|
236
|
+
const covered = grp && grp.satisfied && r.state !== 'set' ? ` (not needed: another ${r.group} option is set)` : '';
|
|
237
|
+
const detail = r.state === 'set' ? `via ${r.satisfiedBy.join(', ')}`
|
|
238
|
+
: r.state === 'unknown' ? 'file unreadable'
|
|
239
|
+
: `${r.state === 'empty' ? 'set but EMPTY' : 'not set'}: ${r.missing.join(', ')}`;
|
|
240
|
+
lines.push(` ${mark[r.state]} ${r.id.padEnd(20)} ${r.need.padEnd(11)} ${detail}${covered}`);
|
|
241
|
+
}
|
|
242
|
+
lines.push('');
|
|
243
|
+
}
|
|
244
|
+
if (result.requiredMissing.length) lines.push(`REQUIRED and missing: ${result.requiredMissing.join(', ')} — see docs/credentials.md for what each is and where it comes from.`);
|
|
245
|
+
if (result.unreadable.length) lines.push('Some env files could not be read, so this answer is incomplete — rerun as a user that can read them (usually sudo).');
|
|
246
|
+
if (result.ok) lines.push('Every required credential is present.');
|
|
247
|
+
lines.push('Deeper check of the publish credential (can it read Actions?): node scripts/gds/publish-credential-check.js');
|
|
248
|
+
return lines.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// readFiles(etc) → the `files` shape sweep() takes, read from <etc>/<name>.
|
|
252
|
+
function readFiles(etc, { fsImpl = fs } = {}) {
|
|
253
|
+
const files = {};
|
|
254
|
+
for (const name of ENV_FILES) {
|
|
255
|
+
const p = path.join(etc, name);
|
|
256
|
+
try {
|
|
257
|
+
files[name] = { env: parseEnvFile(fsImpl.readFileSync(p, 'utf8')) };
|
|
258
|
+
} catch (e) {
|
|
259
|
+
// An absent spaces.env just means offsite backups were never set up; an absent
|
|
260
|
+
// web.env means this is not the host (or the wrong --etc).
|
|
261
|
+
files[name] = e.code === 'ENOENT' && name !== 'web.env' ? { env: {} } : { error: e.code || e.message };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return files;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = { CREDENTIALS, ENV_FILES, parseEnvFile, checkRow, sweep, format, readFiles };
|
|
268
|
+
|
|
269
|
+
if (require.main === module) {
|
|
270
|
+
const etcArg = arg('--etc');
|
|
271
|
+
const etc = etcArg && etcArg !== true ? etcArg : path.join('/etc', configDirName());
|
|
272
|
+
const files = readFiles(etc);
|
|
273
|
+
const keyArg = arg('--secret-key-file');
|
|
274
|
+
const keyFile = keyArg && keyArg !== true ? keyArg : configPath('builder-secret.key');
|
|
275
|
+
let keyFileExists = false;
|
|
276
|
+
try { keyFileExists = fs.statSync(keyFile).isFile(); } catch (_) { /* absent */ }
|
|
277
|
+
const result = sweep({ files, keyFileExists });
|
|
278
|
+
if (hasFlag('--json')) console.log(JSON.stringify({ etc, ...result }, null, 2));
|
|
279
|
+
else console.log(format(result, { etc }));
|
|
280
|
+
cliExit(result.unreadable.length ? 2 : result.ok ? 0 : 1);
|
|
281
|
+
}
|
|
@@ -131,7 +131,10 @@ pg_dump \
|
|
|
131
131
|
# A SIGKILL mid-stream leaves a .partial that the next run will overwrite.
|
|
132
132
|
mv "$OUT.partial" "$OUT"
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
# Size in bytes via POSIX `wc -c`, not `stat -c '%s'`: that is GNU-only, and BSD/macOS
|
|
135
|
+
# stat refuses it outright, so every run on a Mac died here (task 1004170). tr drops the
|
|
136
|
+
# padding BSD wc prints in front of the number.
|
|
137
|
+
BYTES=$(wc -c < "$OUT" | tr -d '[:space:]')
|
|
135
138
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: wrote ${OUT} (${BYTES} bytes)"
|
|
136
139
|
|
|
137
140
|
# Completeness guard (incident 2026-07-02, task 1711; task 1003925). pipefail already
|
|
@@ -190,7 +193,9 @@ ATTACH_OUT="${BACKUP_DIR}/${DB_NAME}-attachments-${TS}.tar.gz"
|
|
|
190
193
|
ATTACH_GLOB="${DB_NAME}-attachments-${D}${D}${D}${D}-${D}${D}-${D}${D}T${D}${D}${D}${D}${D}${D}Z.tar.gz"
|
|
191
194
|
|
|
192
195
|
if [ -d "$ATTACH_DIR" ]; then
|
|
193
|
-
|
|
196
|
+
# Every count here goes through tr for the same reason as BYTES: BSD wc pads it, which
|
|
197
|
+
# put "pruned 1 dump(s)" in a Mac's log where Linux writes "pruned 1 dump(s)".
|
|
198
|
+
ATTACH_COUNT=$(find "$ATTACH_DIR" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')
|
|
194
199
|
if [ "$ATTACH_COUNT" -gt 0 ]; then
|
|
195
200
|
if tar -czf "$ATTACH_OUT.partial" -C "$ATTACH_DIR" . 2>/dev/null; then
|
|
196
201
|
mv "$ATTACH_OUT.partial" "$ATTACH_OUT"
|
|
@@ -215,7 +220,7 @@ fi
|
|
|
215
220
|
PRUNED=$(find "$BACKUP_DIR" -maxdepth 1 -type f \
|
|
216
221
|
\( -name "$DUMP_GLOB" -o -name "$ATTACH_GLOB" \) \
|
|
217
222
|
-mtime +"${RETENTION_DAYS}" \
|
|
218
|
-
-print -delete | wc -l)
|
|
223
|
+
-print -delete | wc -l | tr -d '[:space:]')
|
|
219
224
|
|
|
220
225
|
if [ "$PRUNED" -gt 0 ]; then
|
|
221
226
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: pruned ${PRUNED} dump(s) older than ${RETENTION_DAYS}d"
|
|
@@ -223,5 +228,5 @@ fi
|
|
|
223
228
|
|
|
224
229
|
# ---- emit a one-line summary for journald + monitoring ---------------------
|
|
225
230
|
|
|
226
|
-
REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name "$DUMP_GLOB" | wc -l)
|
|
231
|
+
REMAINING=$(find "$BACKUP_DIR" -maxdepth 1 -type f -name "$DUMP_GLOB" | wc -l | tr -d '[:space:]')
|
|
227
232
|
echo "[$(date -u +%FT%TZ)] gds-db-backup: ok · ${REMAINING} dump(s) retained · ${BYTES} bytes latest"
|
|
@@ -169,7 +169,9 @@ if [ -z "$LATEST" ]; then
|
|
|
169
169
|
fi
|
|
170
170
|
|
|
171
171
|
BASENAME=$(basename "$LATEST")
|
|
172
|
-
|
|
172
|
+
# Sizes via POSIX `wc -c`, never GNU-only `stat -c '%s'`, which BSD/macOS stat refuses
|
|
173
|
+
# (task 1004170). tr drops the padding BSD wc prints in front of the number.
|
|
174
|
+
BYTES=$(wc -c < "$LATEST" | tr -d '[:space:]')
|
|
173
175
|
|
|
174
176
|
# ---- encrypt before it leaves the box ---------------------------------------
|
|
175
177
|
#
|
|
@@ -189,7 +191,7 @@ fi
|
|
|
189
191
|
|
|
190
192
|
# A zero-byte or missing ciphertext would upload happily and restore to nothing.
|
|
191
193
|
# Checked here rather than trusted, because the failure is silent for a month.
|
|
192
|
-
ENC_BYTES=$(
|
|
194
|
+
ENC_BYTES=$(wc -c < "$ENC" | tr -d '[:space:]')
|
|
193
195
|
if [ "$ENC_BYTES" -lt 1 ]; then
|
|
194
196
|
echo "ERROR: encryption produced an empty file — refusing to upload it as a backup." >&2
|
|
195
197
|
exit 2
|
|
@@ -42,9 +42,13 @@
|
|
|
42
42
|
|
|
43
43
|
set -eu
|
|
44
44
|
|
|
45
|
+
# The range rides main-audit.js's --range flag, never an env var (task 1001441): a
|
|
46
|
+
# shell cannot compute the instance's branded env prefix (<PREFIX>_AUDIT_RANGE), and
|
|
47
|
+
# argv needs none. AUDIT_RANGE below is this script's own variable, not exported.
|
|
48
|
+
AUDIT_RANGE=""
|
|
45
49
|
if [ -n "${OTB_DEPLOY_PREV_SHA:-}" ]; then
|
|
46
|
-
|
|
47
|
-
echo "main audit: range ${
|
|
50
|
+
AUDIT_RANGE="${OTB_DEPLOY_PREV_SHA}..HEAD"
|
|
51
|
+
echo "main audit: range ${AUDIT_RANGE} (exact deploy delta)"
|
|
48
52
|
else
|
|
49
53
|
echo "main audit: OTB_DEPLOY_PREV_SHA not set — falling back to the wide window."
|
|
50
54
|
echo " (Recommended: capture OTB_DEPLOY_PREV_SHA=\$(git rev-parse HEAD) BEFORE the pull;"
|
|
@@ -64,7 +68,10 @@ if [ ! -f "$MAIN_AUDIT" ]; then
|
|
|
64
68
|
exit 1
|
|
65
69
|
fi
|
|
66
70
|
|
|
67
|
-
if
|
|
71
|
+
if [ -n "$AUDIT_RANGE" ]; then set -- --range "$AUDIT_RANGE"; else set --; fi
|
|
72
|
+
# ${1+"$@"}, not "$@": an empty "$@" under set -u is an unbound-variable error in
|
|
73
|
+
# older /bin/sh (bash 3.2, macOS).
|
|
74
|
+
if node "$MAIN_AUDIT" --verify-head --db ${STRICT} ${1+"$@"}; then
|
|
68
75
|
echo "OK main audit clean${STRICT:+ (strict)} — proceeding with deploy."
|
|
69
76
|
exit 0
|
|
70
77
|
fi
|
|
@@ -72,7 +79,7 @@ fi
|
|
|
72
79
|
echo ""
|
|
73
80
|
echo "X DEPLOY ABORTED (SEC #863): a below-Metic${STRICT:+ or unresolved} author modified a"
|
|
74
81
|
echo " permission-sensitive path in this deploy. Investigate and revert before deploying:"
|
|
75
|
-
echo " node $MAIN_AUDIT --range \"\${
|
|
82
|
+
echo " node $MAIN_AUDIT --range \"\${AUDIT_RANGE:-HEAD~200..HEAD}\" --revert"
|
|
76
83
|
|
|
77
84
|
if [ -n "${OTB_DEPLOY_PREV_SHA:-}" ] && [ "${OTB_GATE_NO_RESTORE:-0}" != "1" ]; then
|
|
78
85
|
if git rev-parse --verify --quiet "${OTB_DEPLOY_PREV_SHA}^{commit}" >/dev/null; then
|