@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.
Files changed (73) hide show
  1. package/.bongos-core.json +31 -291
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/api/openapi.json +4 -2
  4. package/docs/architecture.md +0 -1
  5. package/docs/file-map.md +0 -1
  6. package/docs/module-api-changelog.md +4 -0
  7. package/modules/lifecycle/routes/tasks.js +18 -4
  8. package/package-lock.json +2 -2
  9. package/package.json +1 -1
  10. package/scripts/gds/ship-land.js +103 -14
  11. package/scripts/gds/ship-merge.js +28 -0
  12. package/scripts/gds/ship.js +8 -0
  13. package/src/bongos/serve-internal.js +10 -56
  14. package/src/module-api.js +1 -1
  15. package/src/platform-server.js +0 -1
  16. package/tests/host_topology_skips.mjs +0 -1
  17. package/tests/instance_release_config.mjs +27 -26
  18. package/tests/publish_branch_route.mjs +70 -2
  19. package/tests/session_rename_fallback.mjs +8 -90
  20. package/tests/ship_land_proof.mjs +218 -0
  21. package/modules/dev-box/app/README.md +0 -74
  22. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  23. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  24. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  25. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  26. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  27. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  28. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  29. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  30. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  31. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  32. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  33. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  34. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  35. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  36. package/modules/dev-box/app/build-resources/icon.png +0 -0
  37. package/modules/dev-box/app/electron-builder.tmpl.yml +0 -95
  38. package/modules/dev-box/app/electron-builder.yml +0 -95
  39. package/modules/dev-box/app/package-lock.json +0 -5291
  40. package/modules/dev-box/app/package.json +0 -30
  41. package/modules/dev-box/app/renderer/app.js +0 -319
  42. package/modules/dev-box/app/renderer/index.html +0 -45
  43. package/modules/dev-box/app/renderer/styles.css +0 -248
  44. package/modules/dev-box/app/renderer/theme.generated.css +0 -16
  45. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +0 -82
  46. package/modules/dev-box/app/scripts/dmgbuild-settings.py +0 -39
  47. package/modules/dev-box/app/scripts/gen-brand.js +0 -138
  48. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +0 -323
  49. package/modules/dev-box/app/scripts/gen-icons.js +0 -286
  50. package/modules/dev-box/app/scripts/vendor-client.js +0 -23
  51. package/modules/dev-box/app/src/box-controller.js +0 -287
  52. package/modules/dev-box/app/src/brand.generated.json +0 -13
  53. package/modules/dev-box/app/src/brand.js +0 -16
  54. package/modules/dev-box/app/src/config.js +0 -31
  55. package/modules/dev-box/app/src/gds-client.js +0 -96
  56. package/modules/dev-box/app/src/main.js +0 -610
  57. package/modules/dev-box/app/src/pairing.js +0 -57
  58. package/modules/dev-box/app/src/preload.js +0 -29
  59. package/modules/dev-box/app/src/session-store.js +0 -82
  60. package/modules/dev-box/app/src/ssh-setup.js +0 -569
  61. package/modules/dev-box/app/src/task-picker.js +0 -70
  62. package/modules/dev-box/app/src/tray.js +0 -99
  63. package/modules/dev-box/app/src/updater.js +0 -55
  64. package/modules/dev-box/app/src/vendor/bongos-client.cjs +0 -1014
  65. package/modules/dev-box/app/test/unit.mjs +0 -526
  66. package/src/devbox-downloads.js +0 -67
  67. package/tests/devbox_artifact_gate.mjs +0 -75
  68. package/tests/devbox_dmg_backdrop.mjs +0 -137
  69. package/tests/devbox_downloads.mjs +0 -140
  70. package/tests/devbox_icons.mjs +0 -304
  71. package/tests/devbox_installer_brand.mjs +0 -232
  72. package/tests/devbox_key_register.mjs +0 -234
  73. 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
- // Three of the read-lists are DUPLICATED rather than imported, because their modules cannot reach
16
- // the canonical one (dependency-free middleware, a separately-bundled Electron app). Those copies
17
- // are pinned here against the real list by parsing the source — an unpinned copy is how a rename
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.
@@ -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