@bongos/core 1.19.883 → 1.19.885
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 +31 -291
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +4 -2
- package/docs/architecture.md +0 -1
- package/docs/file-map.md +0 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/routes/tasks.js +18 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-land.js +103 -14
- package/scripts/gds/ship-merge.js +28 -0
- package/scripts/gds/ship.js +8 -0
- package/src/bongos/serve-internal.js +10 -56
- package/src/module-api.js +1 -1
- package/src/platform-server.js +0 -1
- package/tests/host_topology_skips.mjs +0 -1
- package/tests/instance_release_config.mjs +27 -26
- package/tests/publish_branch_route.mjs +70 -2
- package/tests/session_rename_fallback.mjs +8 -90
- package/tests/ship_land_proof.mjs +218 -0
- package/modules/dev-box/app/README.md +0 -74
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +0 -95
- package/modules/dev-box/app/electron-builder.yml +0 -95
- package/modules/dev-box/app/package-lock.json +0 -5291
- package/modules/dev-box/app/package.json +0 -30
- package/modules/dev-box/app/renderer/app.js +0 -319
- package/modules/dev-box/app/renderer/index.html +0 -45
- package/modules/dev-box/app/renderer/styles.css +0 -248
- package/modules/dev-box/app/renderer/theme.generated.css +0 -16
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +0 -82
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +0 -39
- package/modules/dev-box/app/scripts/gen-brand.js +0 -138
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +0 -323
- package/modules/dev-box/app/scripts/gen-icons.js +0 -286
- package/modules/dev-box/app/scripts/vendor-client.js +0 -23
- package/modules/dev-box/app/src/box-controller.js +0 -287
- package/modules/dev-box/app/src/brand.generated.json +0 -13
- package/modules/dev-box/app/src/brand.js +0 -16
- package/modules/dev-box/app/src/config.js +0 -31
- package/modules/dev-box/app/src/gds-client.js +0 -96
- package/modules/dev-box/app/src/main.js +0 -610
- package/modules/dev-box/app/src/pairing.js +0 -57
- package/modules/dev-box/app/src/preload.js +0 -29
- package/modules/dev-box/app/src/session-store.js +0 -82
- package/modules/dev-box/app/src/ssh-setup.js +0 -569
- package/modules/dev-box/app/src/task-picker.js +0 -70
- package/modules/dev-box/app/src/tray.js +0 -99
- package/modules/dev-box/app/src/updater.js +0 -55
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +0 -1014
- package/modules/dev-box/app/test/unit.mjs +0 -526
- package/src/devbox-downloads.js +0 -67
- package/tests/devbox_artifact_gate.mjs +0 -75
- package/tests/devbox_dmg_backdrop.mjs +0 -137
- package/tests/devbox_downloads.mjs +0 -140
- package/tests/devbox_icons.mjs +0 -304
- package/tests/devbox_installer_brand.mjs +0 -232
- package/tests/devbox_key_register.mjs +0 -234
- package/tests/devbox_update_path.mjs +0 -84
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
// So these tests assert the FALLBACK path fires — a happy-path test on the new name alone would
|
|
13
13
|
// pass just as well with the back-compat deleted, which is exactly the regression that matters.
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
// the canonical one (dependency-free middleware,
|
|
17
|
-
//
|
|
15
|
+
// Some of the read-lists are DUPLICATED rather than imported, because their modules cannot reach
|
|
16
|
+
// the canonical one (dependency-free middleware, static browser surfaces). Those copies are
|
|
17
|
+
// pinned here against the real list by parsing the source — an unpinned copy is how a rename
|
|
18
18
|
// half-lands.
|
|
19
19
|
|
|
20
20
|
import test from 'node:test';
|
|
@@ -235,6 +235,11 @@ test('routes/auth.js sets ONLY the head name and derives the clear-list from the
|
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
// ── 5. the duplicated copies are pinned to the canonical lists ──────────────────────────────
|
|
238
|
+
//
|
|
239
|
+
// There used to be a third copy here and a whole section 7 exercising it: the Dev Box Electron
|
|
240
|
+
// app shipped as its own bundle, so it could not import instance-config and carried its own
|
|
241
|
+
// read-list. Task 1003891 deleted that app, so the copy it was pinned against no longer exists
|
|
242
|
+
// and the cases went with it — there is nothing left to drift.
|
|
238
243
|
|
|
239
244
|
test('rate-limit.js — its dependency-free copy of the cookie names matches the real list', () => {
|
|
240
245
|
const src = read('src/bongos/middleware/rate-limit.js');
|
|
@@ -245,19 +250,6 @@ test('rate-limit.js — its dependency-free copy of the cookie names matches the
|
|
|
245
250
|
'the limiter’s copy drifted from src/bongos/auth.js SESSION_COOKIE_NAMES');
|
|
246
251
|
});
|
|
247
252
|
|
|
248
|
-
test('dev-box session-store — its bundled copy of the filenames matches the real list', () => {
|
|
249
|
-
const src = read('modules/dev-box/app/src/session-store.js');
|
|
250
|
-
const head = /const FILE = path\.join\(DIR, '([^']+)'\)/.exec(src);
|
|
251
|
-
assert.ok(head, 'FILE not found in the dev-box session store');
|
|
252
|
-
const legacy = /const FILES_LEGACY = \[([^\]]*)\]/.exec(src);
|
|
253
|
-
assert.ok(legacy, 'FILES_LEGACY not found — the app would stop reading un-migrated sessions');
|
|
254
|
-
const names = [head[1], ...[...legacy[1].matchAll(/'([^']+\.json)'/g)].map((x) => x[1])];
|
|
255
|
-
assert.deepEqual(names, ic.SESSION_FILENAMES,
|
|
256
|
-
'the dev-box app’s copy drifted from src/instance-config.js SESSION_FILENAMES');
|
|
257
|
-
// It must WRITE only the head name.
|
|
258
|
-
assert.doesNotMatch(src, /writeFileSync\(\s*FILES_LEGACY/, 'the app must never write a legacy name');
|
|
259
|
-
});
|
|
260
|
-
|
|
261
253
|
test('the browser logout clears every session-cookie name', () => {
|
|
262
254
|
// Three static surfaces expire the cookie client-side. One that misses the current name leaves a
|
|
263
255
|
// live cookie behind after "sign out".
|
|
@@ -288,80 +280,6 @@ test('the baked box scripts scan BOTH session filenames', () => {
|
|
|
288
280
|
}
|
|
289
281
|
});
|
|
290
282
|
|
|
291
|
-
// ── 7. the dev-box app's own copy of the fallback, and its sign-out ─────────────────────────
|
|
292
|
-
//
|
|
293
|
-
// The Electron app shares the CLI's session file on purpose (ADR 0045). It ships as its own
|
|
294
|
-
// bundle, so it cannot import instance-config and carries its own read-list — which means its
|
|
295
|
-
// fallback needs exercising, not just pinning.
|
|
296
|
-
function inDevBoxSandbox(body) {
|
|
297
|
-
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-devbox-'));
|
|
298
|
-
try {
|
|
299
|
-
const code = `const R=${JSON.stringify(REPO_ROOT)};
|
|
300
|
-
const SANDBOX=${JSON.stringify(home)};
|
|
301
|
-
const os=require('node:os');
|
|
302
|
-
if (os.homedir() !== SANDBOX) {
|
|
303
|
-
console.error('ERR sandbox-escape: os.homedir()=' + os.homedir() + ' but the sandbox is ' + SANDBOX);
|
|
304
|
-
process.exit(2);
|
|
305
|
-
}
|
|
306
|
-
const fs=require('node:fs'), path=require('node:path');
|
|
307
|
-
const brand=require(R+'/modules/dev-box/app/src/brand.js');
|
|
308
|
-
const CFG=path.join(os.homedir(), '.config', brand.CONFIG_DIR);
|
|
309
|
-
const store=require(R+'/modules/dev-box/app/src/session-store.js');
|
|
310
|
-
const w=(name,obj)=>{fs.mkdirSync(CFG,{recursive:true});fs.writeFileSync(path.join(CFG,name),JSON.stringify(obj,null,2));};
|
|
311
|
-
const has=(name)=>fs.existsSync(path.join(CFG,name));
|
|
312
|
-
const j=(name)=>JSON.parse(fs.readFileSync(path.join(CFG,name),'utf8'));
|
|
313
|
-
(async()=>{ ${body} })().catch((e)=>{ console.error('ERR '+e.message); process.exit(1); });`;
|
|
314
|
-
const r = spawnSync(process.execPath, ['-e', code], { encoding: 'utf8', env: sandboxEnv(home) });
|
|
315
|
-
return { out: `${r.stdout || ''}${r.stderr || ''}`, status: r.status };
|
|
316
|
-
} finally {
|
|
317
|
-
fs.rmSync(home, { recursive: true, force: true });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
test('dev-box: reads an old-name session, and writes migrate to the new name', () => {
|
|
322
|
-
const { out, status } = inDevBoxSandbox(`
|
|
323
|
-
w('gds-session.json', { token:'TOK-OLD', api_base:'https://x.example.com', gemini_api_key:'KEEP' });
|
|
324
|
-
const got = store.read();
|
|
325
|
-
store.write({ token:'TOK-NEW', apiBase:'https://x.example.com' });
|
|
326
|
-
console.log(JSON.stringify({
|
|
327
|
-
readOld: got && got.token,
|
|
328
|
-
newFile: has('bongos-session.json') && j('bongos-session.json').token,
|
|
329
|
-
keptExtra: has('bongos-session.json') && j('bongos-session.json').gemini_api_key,
|
|
330
|
-
oldKept: has('gds-session.json'),
|
|
331
|
-
}));`);
|
|
332
|
-
assert.equal(status, 0, out);
|
|
333
|
-
const r = JSON.parse(out.trim().split('\n').pop());
|
|
334
|
-
assert.equal(r.readOld, 'TOK-OLD', 'the app must still read an un-migrated session');
|
|
335
|
-
assert.equal(r.newFile, 'TOK-NEW', 'a write must land on the new name');
|
|
336
|
-
assert.equal(r.keptExtra, 'KEEP', 'merge-preserving write must carry unknown fields across the rename');
|
|
337
|
-
assert.equal(r.oldKept, true, 'the app must not delete the old file either');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
test('dev-box: sign-out strips the credential from EVERY accepted filename', () => {
|
|
341
|
-
// THE BUG THIS PINS. clear() rewrote only the CURRENT filename, but read() falls through to an
|
|
342
|
-
// older name when the current file carries no token — so sign-out left a live token in the
|
|
343
|
-
// superseded file and the very next read signed the builder straight back in. Harmless while
|
|
344
|
-
// the only legacy name was the long-dead pms-session.json; the common case the moment
|
|
345
|
-
// gds-session.json joined the read-list (task 1003704).
|
|
346
|
-
const { out, status } = inDevBoxSandbox(`
|
|
347
|
-
w('bongos-session.json', { token:'TOK-NEW', api_base:'https://x.example.com' });
|
|
348
|
-
w('gds-session.json', { token:'TOK-OLD', api_base:'https://x.example.com', gemini_api_key:'KEEP' });
|
|
349
|
-
store.clear();
|
|
350
|
-
console.log(JSON.stringify({
|
|
351
|
-
stillSignedIn: !!store.read(),
|
|
352
|
-
newToken: j('bongos-session.json').token ?? null,
|
|
353
|
-
oldToken: j('gds-session.json').token ?? null,
|
|
354
|
-
keptExtra: j('gds-session.json').gemini_api_key,
|
|
355
|
-
}));`);
|
|
356
|
-
assert.equal(status, 0, out);
|
|
357
|
-
const r = JSON.parse(out.trim().split('\n').pop());
|
|
358
|
-
assert.equal(r.stillSignedIn, false, 'sign-out must not leave a readable session under ANY name');
|
|
359
|
-
assert.equal(r.newToken, null, 'the current file must lose its token');
|
|
360
|
-
assert.equal(r.oldToken, null, 'the superseded file must lose its token too — this is the bug');
|
|
361
|
-
// Sign-out drops credentials only; CLI-adjacent config must survive in every file it touches.
|
|
362
|
-
assert.equal(r.keptExtra, 'KEEP', 'clear() must preserve non-credential fields');
|
|
363
|
-
});
|
|
364
|
-
|
|
365
283
|
// ── 8. the credential detector follows the rename ───────────────────────────────────────────
|
|
366
284
|
|
|
367
285
|
test('the artifact-scan credential detector flags a read of EVERY session filename', () => {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// tests/ship_land_proof.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1004055 — the ship that merges to main and then refuses to record it.
|
|
4
|
+
//
|
|
5
|
+
// ship.js has three land paths. Two of them tell the server which commit is this
|
|
6
|
+
// task's: ciLandLocal posts attest-gate after its push, ciLandServer gets it from
|
|
7
|
+
// publish-branch. The LOCAL-merge path (mergeLocally, 'laptop' deploy mode) merged
|
|
8
|
+
// into main and pushed it ITSELF, and told the server nothing — so
|
|
9
|
+
// claims.published_head_sha stayed NULL. It also opens no PR and deletes the branch
|
|
10
|
+
// on origin, so getPublishStatus finds nothing by head ref either. That left
|
|
11
|
+
// deriveShipAssurance with neither of its two proofs, and the POST /ship three lines
|
|
12
|
+
// later refused with ship_land_not_proven EVERY time, with the work already merged.
|
|
13
|
+
// The task parked at 'confirmed', gating its builder's whole queue, until someone ran
|
|
14
|
+
// attest-gate + ship by hand. Five consecutive ships on 2026-09-19; twelve refusals
|
|
15
|
+
// in that day's audit log.
|
|
16
|
+
//
|
|
17
|
+
// What is pinned here:
|
|
18
|
+
// 1. attestGateTrust REPORTS whether the tip landed, and never throws
|
|
19
|
+
// 2. landProofRecorded's truth table — including the pinned-older-core fallback
|
|
20
|
+
// 3. the warning is in the right TENSE for the path that emits it
|
|
21
|
+
// 4. the local-merge path records a tip, before it deletes the branch
|
|
22
|
+
//
|
|
23
|
+
// DB-free and network-free: the only collaborator attestGateTrust has is cli-lib's
|
|
24
|
+
// cliClient, stubbed through the require cache below. Nothing here spawns git, gh,
|
|
25
|
+
// or a server.
|
|
26
|
+
//
|
|
27
|
+
// Run: node tests/ship_land_proof.mjs
|
|
28
|
+
|
|
29
|
+
import { strict as assert } from 'node:assert';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const require = createRequire(import.meta.url);
|
|
35
|
+
|
|
36
|
+
// The stub MUST be in place before ship-land.js is first required — it destructures
|
|
37
|
+
// cliClient at module load.
|
|
38
|
+
let nextAttest = null;
|
|
39
|
+
const cliLibPath = require.resolve('../scripts/gds/cli-lib');
|
|
40
|
+
require.cache[cliLibPath] = {
|
|
41
|
+
id: cliLibPath,
|
|
42
|
+
filename: cliLibPath,
|
|
43
|
+
loaded: true,
|
|
44
|
+
exports: {
|
|
45
|
+
cliClient: async () => ({
|
|
46
|
+
tasks: {
|
|
47
|
+
postTasksIdAttestGate: async ({ id, body }) => {
|
|
48
|
+
if (typeof nextAttest === 'function') return nextAttest({ id, body });
|
|
49
|
+
return nextAttest;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
isBoxScope: () => false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const { attestGateTrust, landProofRecorded, landProofWarningLines } = require('../scripts/gds/ship-land.js');
|
|
58
|
+
|
|
59
|
+
let passed = 0;
|
|
60
|
+
let failed = 0;
|
|
61
|
+
async function t(name, fn) {
|
|
62
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
63
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// attestGateTrust narrates to stdout; swallow that so the test output stays readable.
|
|
67
|
+
async function quietly(fn) {
|
|
68
|
+
const real = console.log;
|
|
69
|
+
const said = [];
|
|
70
|
+
console.log = (...a) => said.push(a.join(' '));
|
|
71
|
+
try { return { value: await fn(), said: said.join('\n') }; }
|
|
72
|
+
finally { console.log = real; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const MERGE_SHA = '5c4db10e2a1f4c6b8d3e9f70a1b2c3d4e5f60718';
|
|
76
|
+
|
|
77
|
+
// --- 1. attestGateTrust reports, and never throws -----------------------------
|
|
78
|
+
|
|
79
|
+
await t('a 200 carrying tip_recorded:true → recorded, and the sha is what we sent', async () => {
|
|
80
|
+
let seen = null;
|
|
81
|
+
nextAttest = ({ id, body }) => {
|
|
82
|
+
seen = { id, body };
|
|
83
|
+
return { ok: true, status: 200, data: { ok: true, attested: true, reason: null, tip_recorded: true } };
|
|
84
|
+
};
|
|
85
|
+
const { value } = await quietly(() => attestGateTrust(1004055, MERGE_SHA));
|
|
86
|
+
assert.equal(value.recorded, true);
|
|
87
|
+
assert.equal(value.attested, true);
|
|
88
|
+
assert.deepEqual(seen, { id: 1004055, body: { head_sha: MERGE_SHA } });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// The case the field exists for: the attestation succeeded, so the old code called
|
|
92
|
+
// the whole thing a success — but no claim row matched, so nothing was recorded and
|
|
93
|
+
// the ship was already doomed.
|
|
94
|
+
await t('a 200 whose claim lookup matched nothing → NOT recorded, even though attested', async () => {
|
|
95
|
+
nextAttest = { ok: true, status: 200, data: { ok: true, attested: true, reason: null, tip_recorded: false } };
|
|
96
|
+
const { value } = await quietly(() => attestGateTrust(1004055, MERGE_SHA));
|
|
97
|
+
assert.equal(value.attested, true, 'the gate attestation did succeed');
|
|
98
|
+
assert.equal(value.recorded, false, 'but the land proof did not land');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await t('a 503 push_unconfigured still recorded the tip (the write precedes the credential check)', async () => {
|
|
102
|
+
nextAttest = { ok: false, status: 503, data: { error: { code: 'push_unconfigured', details: { tip_recorded: true } } } };
|
|
103
|
+
const { value } = await quietly(() => attestGateTrust(1004055, MERGE_SHA));
|
|
104
|
+
assert.equal(value.recorded, true);
|
|
105
|
+
assert.equal(value.attested, false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await t('a 403 never reached the write → NOT recorded', async () => {
|
|
109
|
+
nextAttest = { ok: false, status: 403, data: { error: { code: 'not_your_task' } } };
|
|
110
|
+
const { value } = await quietly(() => attestGateTrust(1004055, MERGE_SHA));
|
|
111
|
+
assert.equal(value.recorded, false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// The quietest of the old failure shapes: a bare `catch (_) {}` left no trace at all.
|
|
115
|
+
await t('a thrown request is reported, not swallowed, and never propagates', async () => {
|
|
116
|
+
nextAttest = () => { throw new Error('fetch failed'); };
|
|
117
|
+
const { value, said } = await quietly(() => attestGateTrust(1004055, MERGE_SHA));
|
|
118
|
+
assert.equal(value.recorded, false);
|
|
119
|
+
assert.match(value.reason, /fetch failed/);
|
|
120
|
+
assert.match(said, /fetch failed/, 'the failure must reach the log too, not only the return value');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await t('no head SHA → not recorded, and no request is attempted', async () => {
|
|
124
|
+
nextAttest = () => { throw new Error('should not have been called'); };
|
|
125
|
+
const { value } = await quietly(() => attestGateTrust(1004055, null));
|
|
126
|
+
assert.equal(value.recorded, false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// --- 2. landProofRecorded: the truth table ------------------------------------
|
|
130
|
+
|
|
131
|
+
await t('explicit tip_recorded wins over any status reading', () => {
|
|
132
|
+
assert.equal(landProofRecorded({ ok: true, status: 200, data: { tip_recorded: true } }), true);
|
|
133
|
+
assert.equal(landProofRecorded({ ok: true, status: 200, data: { tip_recorded: false } }), false);
|
|
134
|
+
assert.equal(landProofRecorded({ ok: false, status: 503, data: { error: { details: { tip_recorded: false } } } }), false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// A live instance runs a PINNED core, so it lags main by however long the pin sits
|
|
138
|
+
// still — cloudbongos.com was serving 1.19.871 against a main declaring 1.19.881 on
|
|
139
|
+
// the day this was diagnosed. Those servers send no tip_recorded at all, and reading
|
|
140
|
+
// its absence as "not recorded" would make every healthy land cry wolf.
|
|
141
|
+
await t('older core, no tip_recorded: 200 and 503 both mean the tip landed', () => {
|
|
142
|
+
assert.equal(landProofRecorded({ ok: true, status: 200, data: { ok: true, attested: false, reason: 'rank_below_metic' } }), true);
|
|
143
|
+
assert.equal(landProofRecorded({ ok: false, status: 503, data: { error: { code: 'push_unconfigured' } } }), true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await t('older core: every other status → NOT recorded', () => {
|
|
147
|
+
for (const status of [400, 403, 404, 409, 500, 502]) {
|
|
148
|
+
assert.equal(
|
|
149
|
+
landProofRecorded({ ok: false, status, data: { error: { code: 'not_your_task' } } }),
|
|
150
|
+
false,
|
|
151
|
+
`HTTP ${status} must not count as a recorded tip`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await t('no response at all → NOT recorded', () => {
|
|
157
|
+
assert.equal(landProofRecorded(null), false);
|
|
158
|
+
assert.equal(landProofRecorded(undefined), false);
|
|
159
|
+
assert.equal(landProofRecorded({}), false);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// --- 3. the warning is in the right tense -------------------------------------
|
|
163
|
+
|
|
164
|
+
await t('merged path: names the merge sha and gives the full two-call recovery', () => {
|
|
165
|
+
const lines = landProofWarningLines(1004055, MERGE_SHA).join('\n');
|
|
166
|
+
assert.match(lines, /NOT recorded/);
|
|
167
|
+
assert.match(lines, /merge IS on main/);
|
|
168
|
+
assert.ok(lines.includes(`"head_sha":"${MERGE_SHA}"`), 'must name the exact sha to re-record');
|
|
169
|
+
assert.match(lines, /POST \/tasks\/1004055\/attest-gate/);
|
|
170
|
+
assert.match(lines, /POST \/tasks\/1004055\/ship/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// The PR path has only PUSHED — the land may still be minutes out, and telling a
|
|
174
|
+
// builder their merge is on main when it is not is the same class of lie this whole
|
|
175
|
+
// task is about.
|
|
176
|
+
await t('un-merged path: does not claim the work is on main, and does not ship blind', () => {
|
|
177
|
+
const lines = landProofWarningLines(1004055, MERGE_SHA, 'HTTP 403', { merged: false }).join('\n');
|
|
178
|
+
assert.doesNotMatch(lines, /merge IS on main/);
|
|
179
|
+
assert.match(lines, /the PR still lands normally/);
|
|
180
|
+
assert.ok(!lines.includes('/ship --body'), 'must not hand over a ship call before anything merged');
|
|
181
|
+
assert.match(lines, /ship\.js 1004055/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await t('the reason is surfaced, and a missing sha degrades to a placeholder', () => {
|
|
185
|
+
assert.match(landProofWarningLines(7, MERGE_SHA, 'HTTP 403').join('\n'), /HTTP 403/);
|
|
186
|
+
assert.match(landProofWarningLines(7, null).join('\n'), /<the merge commit on main>/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// --- 4. the local-merge path records a tip at all -----------------------------
|
|
190
|
+
//
|
|
191
|
+
// mergeLocally has no dependency seam — it merges a real second checkout, runs smoke
|
|
192
|
+
// and pushes main — so this is asserted against the source. Weaker than driving it,
|
|
193
|
+
// but it is exactly the assertion that failed before the fix: the file contained no
|
|
194
|
+
// reference to ANY of the three ways to record a tip, and the order of the two calls
|
|
195
|
+
// is the other half of the bug (deleting the branch first destroys the only evidence
|
|
196
|
+
// a later sweep could have used).
|
|
197
|
+
|
|
198
|
+
const mergeSrc = fs.readFileSync(fileURLToPath(new URL('../scripts/gds/ship-merge.js', import.meta.url)), 'utf8');
|
|
199
|
+
|
|
200
|
+
await t('ship-merge.js records the merge commit as this task\'s land proof', () => {
|
|
201
|
+
assert.match(mergeSrc, /attestGateTrust\(taskId, mergeSha\)/,
|
|
202
|
+
'the local-merge path must record a land proof — before the fix it recorded none');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await t('the proof is recorded BEFORE the origin branch is deleted', () => {
|
|
206
|
+
const attestAt = mergeSrc.indexOf('attestGateTrust(taskId, mergeSha)');
|
|
207
|
+
const deleteAt = mergeSrc.indexOf("'--delete'");
|
|
208
|
+
assert.ok(attestAt > 0, 'the attest call must be present');
|
|
209
|
+
assert.ok(deleteAt > 0, 'the branch delete must be present');
|
|
210
|
+
assert.ok(attestAt < deleteAt, 'the land proof must be recorded before the branch is deleted');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await t('a miss reaches the builder at the merge, not as an opaque 409 later', () => {
|
|
214
|
+
assert.match(mergeSrc, /landProofWarningLines\(taskId, mergeSha, proof\.reason\)/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
218
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# OTB Dev Box — the desktop switch for a builder's hosted box
|
|
2
|
-
|
|
3
|
-
A menu-bar (macOS) / system-tray (Windows) app that turns a builder's hosted
|
|
4
|
-
dev box on and off with one switch. Built for the newcomer who should never
|
|
5
|
-
see a terminal: sign in with browser clicks (app pairing, ADR 0045), flip the
|
|
6
|
-
switch, and connect from Claude Desktop or the browser terminal.
|
|
7
|
-
|
|
8
|
-
What it does beyond the switch — each fixing a failure observed in the
|
|
9
|
-
2026-06-10 onboarding sessions that killed the old `curl | bash` flow:
|
|
10
|
-
|
|
11
|
-
- **Pairing sign-in** — opens the builders' hall, one Approve click, token
|
|
12
|
-
delivered over the app's own secret channel (`/api/bongos/auth/app-pair/*`).
|
|
13
|
-
The session is written to `~/.config/otb/gds-session.json`, the same file
|
|
14
|
-
the `/builder-*` CLI uses — pairing the app signs in the CLI too.
|
|
15
|
-
- **SSH key done right** — generates `~/.ssh/otb_builder` (ed25519) with real
|
|
16
|
-
600 perms (icacls ACL on Windows) and registers it via `POST /box/ssh-key`.
|
|
17
|
-
- **known_hosts kept fresh** — Claude Desktop's embedded ssh2 reads ONLY
|
|
18
|
-
`~/.ssh/known_hosts`, and host keys change on every re-provision; the app
|
|
19
|
-
re-scans and replaces entries each time the box turns active.
|
|
20
|
-
- **Claude Desktop connection file** — installs the managed-settings file to
|
|
21
|
-
the system path with a native elevation prompt (osascript / UAC), the step
|
|
22
|
-
the agent-driven flow could never finish.
|
|
23
|
-
- **Native notifications** — box ready, rested while idle.
|
|
24
|
-
- **No heartbeats** — the app never sends `claude_active`; the droplet's idle
|
|
25
|
-
sweep stays authoritative so an unused box still reclaims itself (~10 min).
|
|
26
|
-
|
|
27
|
-
## Layout
|
|
28
|
-
|
|
29
|
-
- `src/main.js` — Electron main: tray, window, IPC, wiring.
|
|
30
|
-
- `src/brand.js` — every brand string (ADR 0004: the civ name lives here only).
|
|
31
|
-
- `src/{pairing,box-controller,gds-client,session-store,ssh-setup,updater}.js` — the
|
|
32
|
-
logic, Electron-free and unit-tested in `test/unit.mjs`.
|
|
33
|
-
- `src/updater.js` — in-app update check (#924): semver compare + platform→target
|
|
34
|
-
resolution. `main.js` polls `GET /downloads/devbox/version` on open + every 12h
|
|
35
|
-
and, when the published release is strictly newer, shows the Update banner; the
|
|
36
|
-
Update button downloads the installer and hands it to the OS (browser-download
|
|
37
|
-
fallback on any error). **Bumping `config/devbox-app.json` version is what makes
|
|
38
|
-
the banner appear in the field.**
|
|
39
|
-
- `renderer/` — the panel UI (sandboxed, contextIsolated; preload exposes
|
|
40
|
-
named verbs only).
|
|
41
|
-
- `scripts/gen-icons.js` — regenerates all icons from pixel grids (`npm run
|
|
42
|
-
icons`; borrows `sharp` from the repo root's node_modules).
|
|
43
|
-
|
|
44
|
-
## Develop
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
cd devbox-app
|
|
48
|
-
npm install
|
|
49
|
-
npm test # hermetic unit tests, no Electron needed
|
|
50
|
-
npm start # run against prod (or OTB_DEVBOX_API_BASE=http://… npm start)
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Dev/test env hooks: `OTB_DEVBOX_API_BASE` (point at a stub/staging GDS),
|
|
54
|
-
`OTB_DEVBOX_NO_ELEVATE=1` (skip the managed-settings elevation prompt),
|
|
55
|
-
`OTB_DEVBOX_SCREENSHOT_DIR` (write a PNG of the panel on every state change —
|
|
56
|
-
how CI/verification captures proof).
|
|
57
|
-
|
|
58
|
-
## Build & release
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
npm run dist:mac # dmg, arm64 + x64 (unsigned → ad-hoc)
|
|
62
|
-
npm run dist:win # NSIS one-click installer, x64 + arm64
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
CI builds both on every push that touches `devbox-app/**`
|
|
66
|
-
(`.github/workflows/build-devbox-app.yml`) and uploads them as artifacts.
|
|
67
|
-
Publishing = attach the artifacts to a release on the PUBLIC releases repo
|
|
68
|
-
named in `config/devbox-app.json`, then bump that config and ship — the
|
|
69
|
-
branded `/downloads/devbox/*` redirects follow the config. (The long-form
|
|
70
|
-
release recipe was retired with the other published dev-box recipes —
|
|
71
|
-
task 1004013, ADR 0305.)
|
|
72
|
-
|
|
73
|
-
This subproject is self-contained on purpose: its `package.json` never feeds
|
|
74
|
-
the droplet's `npm ci`, and nothing under `devbox-app/` runs server-side.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# TEMPLATE — rendered to electron-builder.yml by scripts/gen-brand.js from
|
|
2
|
-
# config/branding.json (@@TOKENS@@ are substituted; ${version}/${arch}/${ext}
|
|
3
|
-
# are electron-builder's own and stay literal). Edit THIS file, never the
|
|
4
|
-
# generated electron-builder.yml.
|
|
5
|
-
#
|
|
6
|
-
# Brand strings (appId, productName, copyright, artifact slug) come from the
|
|
7
|
-
# instance branding so one source tree builds any instance's app (ADR 0004,
|
|
8
|
-
# generalised). Signing rides the instance's own CI (main-ref-gated Apple
|
|
9
|
-
# Developer ID; ADR 0072) — an unarmed build ad-hoc-signs the mac app, which
|
|
10
|
-
# arm64 requires to launch at all.
|
|
11
|
-
#
|
|
12
|
-
# Artifact names are pinned (no spaces, no world name inside — ADR 0004) and
|
|
13
|
-
# MUST match config/devbox-app.json's assets map; tests/devbox_downloads.mjs
|
|
14
|
-
# cross-checks the config side.
|
|
15
|
-
|
|
16
|
-
appId: @@APP_ID@@
|
|
17
|
-
productName: @@PRODUCT_NAME@@
|
|
18
|
-
copyright: @@COPYRIGHT@@
|
|
19
|
-
|
|
20
|
-
# buildResources is named build-resources (not electron-builder's default
|
|
21
|
-
# `build`) because the repo root .gitignore ignores build/ everywhere and the
|
|
22
|
-
# app icon in here is COMMITTED source, not output.
|
|
23
|
-
directories:
|
|
24
|
-
buildResources: build-resources
|
|
25
|
-
output: dist
|
|
26
|
-
|
|
27
|
-
files:
|
|
28
|
-
- src/**/*
|
|
29
|
-
- renderer/**/*
|
|
30
|
-
- assets/**/*
|
|
31
|
-
- package.json
|
|
32
|
-
|
|
33
|
-
asar: true
|
|
34
|
-
|
|
35
|
-
mac:
|
|
36
|
-
category: public.app-category.developer-tools
|
|
37
|
-
icon: build-resources/icon.png
|
|
38
|
-
target:
|
|
39
|
-
- target: dmg
|
|
40
|
-
arch: [arm64, x64]
|
|
41
|
-
artifactName: @@PRODUCT_SLUG@@-${version}-${arch}.${ext}
|
|
42
|
-
|
|
43
|
-
dmg:
|
|
44
|
-
artifactName: @@PRODUCT_SLUG@@-${version}-${arch}.${ext}
|
|
45
|
-
# On-brand install window (task 912). The backdrop
|
|
46
|
-
# (build-resources/dmg-background.png + @2x, drawn by
|
|
47
|
-
# scripts/gen-dmg-backdrop.py) guides the drag from the container (the app)
|
|
48
|
-
# into Applications. iconSize + window + the contents coords MUST match
|
|
49
|
-
# the icon-well centers the backdrop draws (LEFT 180,198 / RIGHT 480,198) — if
|
|
50
|
-
# you move one, re-run the generator. The volume icon is the app icon (the
|
|
51
|
-
# container). electron-builder auto-picks dmg-background@2x.png on Retina.
|
|
52
|
-
#
|
|
53
|
-
# task 1235: this electron-builder DMG is only an INTERMEDIATE. Its window
|
|
54
|
-
# backdrop does NOT render on macOS >= Sonoma — electron-builder writes a
|
|
55
|
-
# `pBBk` bookmark Finder can't resolve, so the install window shows up white.
|
|
56
|
-
# The CI step "Apply install-window backdrop (dmgbuild)" re-packages the
|
|
57
|
-
# signed+notarized .app out of this DMG into the FINAL DMG via dmgbuild
|
|
58
|
-
# (scripts/apply-dmg-backdrop.sh + scripts/dmgbuild-settings.py), which writes
|
|
59
|
-
# an alias-only .DS_Store that DOES render. Keep the coords below in sync with
|
|
60
|
-
# dmgbuild-settings.py (currently identical: 180,198 / 480,198, 660x400, size 100).
|
|
61
|
-
icon: build-resources/icon.png
|
|
62
|
-
background: build-resources/dmg-background.png
|
|
63
|
-
iconSize: 100
|
|
64
|
-
window:
|
|
65
|
-
width: 660
|
|
66
|
-
height: 400
|
|
67
|
-
contents:
|
|
68
|
-
- x: 180
|
|
69
|
-
y: 198
|
|
70
|
-
type: file
|
|
71
|
-
- x: 480
|
|
72
|
-
y: 198
|
|
73
|
-
type: link
|
|
74
|
-
path: /Applications
|
|
75
|
-
|
|
76
|
-
win:
|
|
77
|
-
icon: build-resources/icon.png
|
|
78
|
-
target:
|
|
79
|
-
- target: nsis
|
|
80
|
-
# x64 covers Intel/AMD PCs; arm64 is native for ARM Windows (Surface,
|
|
81
|
-
# Snapdragon laptops, Parallels VMs on Apple-silicon Macs). x64 also
|
|
82
|
-
# RUNS on arm64 via Windows emulation, so x64 stays the safe default
|
|
83
|
-
# link; arm64 is the first-class build for those machines (#908).
|
|
84
|
-
arch: [x64, arm64]
|
|
85
|
-
|
|
86
|
-
nsis:
|
|
87
|
-
oneClick: true
|
|
88
|
-
perMachine: false
|
|
89
|
-
runAfterFinish: true
|
|
90
|
-
deleteAppDataOnUninstall: false
|
|
91
|
-
# ${arch} suffix added in #908 when arm64 joined — two arches can't share
|
|
92
|
-
# one filename. Must stay in lockstep with config/devbox-app.json assets.
|
|
93
|
-
artifactName: @@PRODUCT_SLUG@@-Setup-${version}-${arch}.${ext}
|
|
94
|
-
|
|
95
|
-
publish: null
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# TEMPLATE — rendered to electron-builder.yml by scripts/gen-brand.js from
|
|
2
|
-
# config/branding.json (@@TOKENS@@ are substituted; ${version}/${arch}/${ext}
|
|
3
|
-
# are electron-builder's own and stay literal). Edit THIS file, never the
|
|
4
|
-
# generated electron-builder.yml.
|
|
5
|
-
#
|
|
6
|
-
# Brand strings (appId, productName, copyright, artifact slug) come from the
|
|
7
|
-
# instance branding so one source tree builds any instance's app (ADR 0004,
|
|
8
|
-
# generalised). Signing rides the instance's own CI (main-ref-gated Apple
|
|
9
|
-
# Developer ID; ADR 0072) — an unarmed build ad-hoc-signs the mac app, which
|
|
10
|
-
# arm64 requires to launch at all.
|
|
11
|
-
#
|
|
12
|
-
# Artifact names are pinned (no spaces, no world name inside — ADR 0004) and
|
|
13
|
-
# MUST match config/devbox-app.json's assets map; tests/devbox_downloads.mjs
|
|
14
|
-
# cross-checks the config side.
|
|
15
|
-
|
|
16
|
-
appId: com.cloudbongos.devbox
|
|
17
|
-
productName: Cloud Bongos Dev Box
|
|
18
|
-
copyright: © Cloud Bongos
|
|
19
|
-
|
|
20
|
-
# buildResources is named build-resources (not electron-builder's default
|
|
21
|
-
# `build`) because the repo root .gitignore ignores build/ everywhere and the
|
|
22
|
-
# app icon in here is COMMITTED source, not output.
|
|
23
|
-
directories:
|
|
24
|
-
buildResources: build-resources
|
|
25
|
-
output: dist
|
|
26
|
-
|
|
27
|
-
files:
|
|
28
|
-
- src/**/*
|
|
29
|
-
- renderer/**/*
|
|
30
|
-
- assets/**/*
|
|
31
|
-
- package.json
|
|
32
|
-
|
|
33
|
-
asar: true
|
|
34
|
-
|
|
35
|
-
mac:
|
|
36
|
-
category: public.app-category.developer-tools
|
|
37
|
-
icon: build-resources/icon.png
|
|
38
|
-
target:
|
|
39
|
-
- target: dmg
|
|
40
|
-
arch: [arm64, x64]
|
|
41
|
-
artifactName: Cloud-Bongos-Dev-Box-${version}-${arch}.${ext}
|
|
42
|
-
|
|
43
|
-
dmg:
|
|
44
|
-
artifactName: Cloud-Bongos-Dev-Box-${version}-${arch}.${ext}
|
|
45
|
-
# On-brand install window (task 912). The backdrop
|
|
46
|
-
# (build-resources/dmg-background.png + @2x, drawn by
|
|
47
|
-
# scripts/gen-dmg-backdrop.py) guides the drag from the container (the app)
|
|
48
|
-
# into Applications. iconSize + window + the contents coords MUST match
|
|
49
|
-
# the icon-well centers the backdrop draws (LEFT 180,198 / RIGHT 480,198) — if
|
|
50
|
-
# you move one, re-run the generator. The volume icon is the app icon (the
|
|
51
|
-
# container). electron-builder auto-picks dmg-background@2x.png on Retina.
|
|
52
|
-
#
|
|
53
|
-
# task 1235: this electron-builder DMG is only an INTERMEDIATE. Its window
|
|
54
|
-
# backdrop does NOT render on macOS >= Sonoma — electron-builder writes a
|
|
55
|
-
# `pBBk` bookmark Finder can't resolve, so the install window shows up white.
|
|
56
|
-
# The CI step "Apply install-window backdrop (dmgbuild)" re-packages the
|
|
57
|
-
# signed+notarized .app out of this DMG into the FINAL DMG via dmgbuild
|
|
58
|
-
# (scripts/apply-dmg-backdrop.sh + scripts/dmgbuild-settings.py), which writes
|
|
59
|
-
# an alias-only .DS_Store that DOES render. Keep the coords below in sync with
|
|
60
|
-
# dmgbuild-settings.py (currently identical: 180,198 / 480,198, 660x400, size 100).
|
|
61
|
-
icon: build-resources/icon.png
|
|
62
|
-
background: build-resources/dmg-background.png
|
|
63
|
-
iconSize: 100
|
|
64
|
-
window:
|
|
65
|
-
width: 660
|
|
66
|
-
height: 400
|
|
67
|
-
contents:
|
|
68
|
-
- x: 180
|
|
69
|
-
y: 198
|
|
70
|
-
type: file
|
|
71
|
-
- x: 480
|
|
72
|
-
y: 198
|
|
73
|
-
type: link
|
|
74
|
-
path: /Applications
|
|
75
|
-
|
|
76
|
-
win:
|
|
77
|
-
icon: build-resources/icon.png
|
|
78
|
-
target:
|
|
79
|
-
- target: nsis
|
|
80
|
-
# x64 covers Intel/AMD PCs; arm64 is native for ARM Windows (Surface,
|
|
81
|
-
# Snapdragon laptops, Parallels VMs on Apple-silicon Macs). x64 also
|
|
82
|
-
# RUNS on arm64 via Windows emulation, so x64 stays the safe default
|
|
83
|
-
# link; arm64 is the first-class build for those machines (#908).
|
|
84
|
-
arch: [x64, arm64]
|
|
85
|
-
|
|
86
|
-
nsis:
|
|
87
|
-
oneClick: true
|
|
88
|
-
perMachine: false
|
|
89
|
-
runAfterFinish: true
|
|
90
|
-
deleteAppDataOnUninstall: false
|
|
91
|
-
# ${arch} suffix added in #908 when arm64 joined — two arches can't share
|
|
92
|
-
# one filename. Must stay in lockstep with config/devbox-app.json assets.
|
|
93
|
-
artifactName: Cloud-Bongos-Dev-Box-Setup-${version}-${arch}.${ext}
|
|
94
|
-
|
|
95
|
-
publish: null
|