workbite 0.1.22 → 0.1.24

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/lib/runtime.js CHANGED
@@ -1,775 +1,775 @@
1
- 'use strict';
2
- // WorkBite suite supervisor: starts or reuses the verified Work Stack native
3
- // owner and the StackBite room backend in WorkBite's own state, then runs the
4
- // Work Stack terminal UI in the foreground. Backends outlive the UI so agents
5
- // keep running; nothing here stops, adopts or kills a process it cannot
6
- // authenticate as its own.
7
- const fs = require('node:fs/promises');
8
- const fsSync = require('node:fs');
9
- const http = require('node:http');
10
- const net = require('node:net');
11
- const path = require('node:path');
12
- const { performance } = require('node:perf_hooks');
13
- const { spawn, execFile } = require('node:child_process');
14
- const { randomBytes, randomUUID } = require('node:crypto');
15
- const { fail, exists, safeDirectory, writeAtomic, readJson, readSmall, sha256, UUID, BACKEND_ID } = require('./common');
16
- const { bootIdentity, validBoot, previousBoot } = require('./boot-provenance');
17
- const { withLock } = require('./startup-lock');
18
- const { BUNDLE_V2 } = require('./install');
19
- const { protectPrivateDirectories } = require('./private-state');
20
-
21
- const BACKEND_SCHEMA = 'stackbite.workstack-backend.v1';
22
- const CONNECTION_SCHEMA = 'workstack.helm-connection.v1';
23
- const OWNER_SCHEMA = 'workbite.owner.v2';
24
- const STACKBITE_SCHEMA = 'workbite.stackbite-backend.v2';
25
- const SELECTION_SCHEMA = 'workbite.workspace-selection.v1';
26
- // Foreground UI per open: the familiar TypeScript Work Stack UI by default, the unified Rust UI on request.
27
- const FRONTENDS = Object.freeze(['typescript', 'rust']);
28
- const LOOPBACK = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/;
29
- // Ports of the existing official StackBite and its previews are never chosen.
30
- const RESERVED_PORTS = new Set([8089, 8090, 8091, 54036, 54039, 54236, 54239]);
31
- // Display preferences a child may keep (as Work Stack's viewer environment does);
32
- // every other STACKBITE_/HELM_/OFFICE_ variable is caller Room/Seat routing or auth.
33
- const PREFERENCES = new Set(['STACKBITE_CHROME_PROFILE', 'STACKBITE_CLIPBOARD_OSC52', 'STACKBITE_FRONTEND',
34
- 'STACKBITE_SCROLLBACK_LINES', 'STACKBITE_TERMINAL_CORE', 'STACKBITE_UPDATE_CHECK']);
35
-
36
- function cleanEnvironment(source = process.env) {
37
- const result = {};
38
- for (const [key, value] of Object.entries(source)) {
39
- const upper = key.toUpperCase();
40
- if (!key || value === undefined) continue;
41
- if (/^(STACKBITE_|HELM_|OFFICE_)/.test(upper) && !PREFERENCES.has(upper)) continue;
42
- if (/^(WORKSTACK_|WORK_STACK_|WORKBITE_)/.test(upper) || upper === 'PYINSTALLER_RESET_ENVIRONMENT') continue;
43
- result[key] = value;
44
- }
45
- return result;
46
- }
47
- function withPath(environment, directory) {
48
- const key = Object.keys(environment).find(name => name.toUpperCase() === 'PATH') || 'PATH';
49
- const current = environment[key];
50
- return { ...environment, [key]: current ? directory + path.delimiter + current : directory };
51
- }
52
-
53
- function stateLayout(root) {
54
- return { root, stackbite: path.join(root, 'stackbite'), workstackRuntime: path.join(root, 'workstack-runtime'),
55
- journal: path.join(root, 'deploy-journal'), tuiState: path.join(root, 'workstack-tui'), owners: path.join(root, 'owners'),
56
- logs: path.join(root, 'logs'), dock: path.join(root, 'execution-dock.json'),
57
- stackbiteRecord: path.join(root, 'stackbite-backend.json'), lock: path.join(root, 'open.lock'),
58
- selection: path.join(root, 'last-workspace.json'),
59
- // v2 execution UI only: its private intent journal and device settings, never under a workspace.
60
- nativeTui: path.join(root, 'workstack-native-tui'),
61
- tuiIntent: path.join(root, 'workstack-native-tui', 'intent.json'),
62
- tuiSettings: path.join(root, 'workstack-native-tui', 'tui.json') };
63
- }
64
- async function prepareState(layout) {
65
- for (const directory of [layout.root, layout.stackbite, layout.workstackRuntime, layout.journal, layout.tuiState,
66
- layout.owners, layout.logs]) await safeDirectory(directory);
67
- }
68
-
69
- // The operator pin StackBite reads from <state>/workstack-backend.json; generated once, never replaced.
70
- async function ensureBackendId(layout) {
71
- const file = path.join(layout.stackbite, 'workstack-backend.json');
72
- if (!await exists(file)) {
73
- const backendId = `workbite-${randomBytes(12).toString('hex')}`;
74
- const bytes = Buffer.from(JSON.stringify({ schema: BACKEND_SCHEMA, backend_id: backendId }) + '\n');
75
- try {
76
- const handle = await fs.open(file, 'wx', 0o600);
77
- try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
78
- } catch (error) { if (error.code !== 'EEXIST') throw error; }
79
- }
80
- let document;
81
- try { document = await readJson(file, 4096); } catch { fail('WorkBite StackBite backend identity file is unreadable; inspect it rather than replacing it.'); }
82
- if (document === null || typeof document !== 'object' || Array.isArray(document) || Object.keys(document).length !== 2
83
- || document.schema !== BACKEND_SCHEMA || typeof document.backend_id !== 'string' || !BACKEND_ID.test(document.backend_id)) {
84
- fail('WorkBite StackBite backend identity file is invalid; inspect it rather than replacing it.');
85
- }
86
- return document.backend_id;
87
- }
88
-
89
- function workspaceKey(workspace, platform = process.platform) {
90
- return sha256(Buffer.from(platform === 'win32' ? workspace.toLowerCase() : workspace)).slice(0, 24);
91
- }
92
- function checkedWorkspace(value) {
93
- if (typeof value !== 'string' || !path.isAbsolute(value) || /[\x00-\x1f]/.test(value)
94
- || (process.platform === 'win32' && /^[\\/](?![\\/])/.test(value))) fail('--workspace must be an absolute path');
95
- return path.resolve(value);
96
- }
97
- // The genuine Work Stack descriptor: workspace.json "id". Nothing is fabricated or repaired.
98
- async function readWorkspaceUid(workspace) {
99
- const descriptor = path.join(workspace, 'workspace.json');
100
- if (!await exists(descriptor)) fail('This folder is not a Work Stack workspace (no workspace.json). Use --init only for a new or empty folder.');
101
- let document;
102
- try { document = await readJson(descriptor, 1024 * 1024); } catch { fail('workspace.json is unreadable or not JSON; WorkBite does not repair it.'); }
103
- const uid = document?.id;
104
- if (typeof uid !== 'string' || !UUID.test(uid)) fail('workspace.json has no canonical workspace UUID id.');
105
- return uid;
106
- }
107
-
108
- function boundedText(text) {
109
- const line = String(text).split(/\r?\n/).map(value => value.trim()).filter(Boolean).pop() || '';
110
- return line.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 300);
111
- }
112
- const finiteTimeout = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback;
113
- const remaining = deadline => Math.max(0, deadline - performance.now());
114
- // Bounds observation, not kernel filesystem I/O or the lifetime of an owned backend.
115
- async function beforeDeadline(action, deadline, message) {
116
- const left = remaining(deadline);
117
- if (!left) throw new Error(message);
118
- let timer;
119
- try {
120
- const result = await Promise.race([Promise.resolve().then(action), new Promise((_, reject) => {
121
- timer = setTimeout(() => reject(new Error(message)), left);
122
- })]);
123
- if (!remaining(deadline)) throw new Error(message);
124
- return result;
125
- } finally { clearTimeout(timer); }
126
- }
127
- function runOnce(file, args, environment, timeout = 120000, execImpl = execFile, options = {}) {
128
- timeout = finiteTimeout(timeout, 120000);
129
- return new Promise(resolve => {
130
- let child, settled = false;
131
- const finish = (error, stdout, stderr) => {
132
- if (settled) return;
133
- settled = true;
134
- clearTimeout(timer);
135
- resolve({ error, stdout: stdout || '', stderr: stderr || '' });
136
- };
137
- const timer = setTimeout(() => {
138
- // A kill request is not proof of exit. Refuse with an unknown outcome; never retry.
139
- finish(Object.assign(new Error('startup command timed out; outcome unknown'), { killed: true, code: 'ETIMEDOUT' }));
140
- try { child?.kill('SIGKILL'); } catch { /* outcome remains unknown */ }
141
- }, timeout);
142
- try {
143
- child = execImpl(file, args, { ...options, env: environment, shell: false, windowsHide: true, timeout,
144
- maxBuffer: options.maxBuffer || 1024 * 1024, encoding: 'utf8' }, finish);
145
- } catch (error) { finish(error); }
146
- });
147
- }
148
- async function initializeWorkspace(commands, workspace, environment, { timeout, execImpl } = {}) {
149
- if (await exists(path.join(workspace, 'workspace.json'))) fail('This folder is already a Work Stack workspace; open it without --init.');
150
- if (!await exists(workspace)) {
151
- try { await fs.mkdir(workspace, { mode: 0o700 }); } catch { fail('Cannot create the workspace folder; its parent folder must exist.'); }
152
- }
153
- const uid = randomUUID();
154
- const [file, ...prefix] = commands.workstack_engine;
155
- const result = await runOnce(file, [...prefix, 'init', '--workspace', workspace, '--workspace-uid', uid], environment, timeout, execImpl);
156
- let record;
157
- try { record = JSON.parse(result.stdout.trim()); } catch { record = null; }
158
- if (result.error || record?.state !== 'initialized' || record.workspace_uid !== uid) {
159
- fail(`Work Stack init refused this folder: ${result.error?.killed ? 'timed out; outcome unknown; inspect the workspace before opening again' : boundedText(result.stderr) || 'no ready record'}`);
160
- }
161
- return uid;
162
- }
163
-
164
- // The workspace a bare `workbite` reopens: the last one opened successfully, or, for
165
- // state from 0.1.0 with no saved selection, the single owner record it holds. It is
166
- // never guessed (no CWD, mtime or default folder) and never silently replaced by
167
- // another workspace; the genuine workspace.json id must still match.
168
- const EXPLICIT = 'Open it once with: workbite open --workspace ABS';
169
- async function currentUid(workspace, what) {
170
- try { return await readWorkspaceUid(workspace); } catch (error) {
171
- fail(`The ${what} ${workspace} is missing, moved or no longer a valid Work Stack workspace (${error.message}) ${EXPLICIT}`);
172
- }
173
- }
174
- async function rememberedWorkspace(layout) {
175
- let saved;
176
- try { saved = await readJson(layout.selection, 8192); } catch (error) {
177
- if (error.code !== 'ENOENT') fail(`The saved workspace selection ${layout.selection} is unreadable. ${EXPLICIT}`);
178
- return ownerWorkspace(layout);
179
- }
180
- if (saved === null || typeof saved !== 'object' || Array.isArray(saved) || Object.keys(saved).length !== 3
181
- || saved.schema !== SELECTION_SCHEMA || typeof saved.workspace !== 'string' || !path.isAbsolute(saved.workspace)
182
- || typeof saved.workspace_uid !== 'string' || !UUID.test(saved.workspace_uid)) {
183
- fail(`The saved workspace selection ${layout.selection} is invalid. ${EXPLICIT}`);
184
- }
185
- if (await currentUid(saved.workspace, 'last opened workspace') !== saved.workspace_uid) {
186
- fail(`The last opened workspace ${saved.workspace} was replaced (its workspace.json id changed); WorkBite does not open it silently. ${EXPLICIT}`);
187
- }
188
- return { workspace: saved.workspace, uid: saved.workspace_uid };
189
- }
190
- async function ownerWorkspace(layout) {
191
- let names = [];
192
- try { names = (await fs.readdir(layout.owners)).filter(name => /^[0-9a-f]{24}\.json$/.test(name)); } catch (error) {
193
- if (error.code !== 'ENOENT') fail(`WorkBite owner records are unreadable. ${EXPLICIT}`);
194
- }
195
- if (!names.length) fail(`No workspace has been opened yet. ${EXPLICIT} (add --init for a new or empty folder).`);
196
- if (names.length > 1) fail(`WorkBite has ${names.length} workspace owner records and will not guess which one to open. ${EXPLICIT}`);
197
- const file = path.join(layout.owners, names[0]);
198
- let record;
199
- try { record = await readJson(file); } catch { fail(`The Work Stack owner record ${file} is unreadable. ${EXPLICIT}`); }
200
- if (!isRecord(record) || record.schema !== OWNER_SCHEMA || typeof record.workspace !== 'string' || !path.isAbsolute(record.workspace)
201
- || `${workspaceKey(record.workspace)}.json` !== names[0] || typeof record.workspace_uid !== 'string' || !UUID.test(record.workspace_uid)) {
202
- fail(`The Work Stack owner record ${file} is not a valid owner record. ${EXPLICIT}`);
203
- }
204
- if (await currentUid(record.workspace, 'workspace recorded in the owner record') !== record.workspace_uid) {
205
- fail(`The workspace recorded in ${file} was replaced (its workspace.json id changed); WorkBite does not open it silently. ${EXPLICIT}`);
206
- }
207
- return { workspace: record.workspace, uid: record.workspace_uid };
208
- }
209
- async function rememberWorkspace(layout, workspace, uid) {
210
- const bytes = Buffer.from(JSON.stringify({ schema: SELECTION_SCHEMA, workspace, workspace_uid: uid }, null, 2) + '\n');
211
- let current = null;
212
- try { current = await readSmall(layout.selection, 8192); } catch { current = null; }
213
- if (!current || !current.equals(bytes)) await writeAtomic(layout.selection, bytes);
214
- }
215
-
216
- function request(url, { method = 'GET', headers = {}, body, timeout = 5000 } = {}) {
217
- const match = LOOPBACK.exec(url.origin);
218
- if (!match) return Promise.reject(new Error('not a loopback origin'));
219
- return new Promise((resolve, reject) => {
220
- timeout = finiteTimeout(timeout, 5000);
221
- let timer, responseStream, settled = false;
222
- const finish = (error, value) => {
223
- if (settled) return;
224
- settled = true;
225
- clearTimeout(timer);
226
- if (error) reject(error); else resolve(value);
227
- };
228
- const payload = body === undefined ? undefined : Buffer.from(JSON.stringify(body));
229
- const req = http.request(url, { method, agent: false,
230
- headers: { ...headers, ...(payload ? { 'Content-Type': 'application/json', 'Content-Length': payload.length } : {}) } },
231
- response => {
232
- responseStream = response;
233
- const chunks = [];
234
- let size = 0;
235
- response.on('data', chunk => {
236
- size += chunk.length;
237
- if (size > 1024 * 1024) abort(new Error('response too large')); else chunks.push(chunk);
238
- });
239
- response.on('end', () => {
240
- let parsed = null;
241
- try { parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { parsed = null; }
242
- finish(null, { status: response.statusCode, body: parsed });
243
- });
244
- response.on('error', error => finish(error));
245
- response.on('aborted', () => finish(new Error('response aborted')));
246
- response.on('close', () => { if (!response.complete) finish(new Error('response closed before completion')); });
247
- });
248
- const abort = error => { finish(error); responseStream?.destroy(); req.destroy(); };
249
- // A socket inactivity timeout can be extended forever by trickled bytes.
250
- timer = setTimeout(() => abort(new Error('request timed out')), timeout);
251
- req.on('error', error => finish(error));
252
- req.end(payload);
253
- });
254
- }
255
-
256
- // Owner identity is proven only by the private lifecycle token this WorkBite generated.
257
- async function engineIdentity(endpoint, token, workspaceUid, { timeout = 5000 } = {}) {
258
- if (typeof endpoint !== 'string' || !LOOPBACK.test(endpoint) || typeof token !== 'string' || !token) return false;
259
- try {
260
- const answer = await request(new URL('/api/v1/native-runtime/identity', endpoint), { timeout, headers: { 'X-WorkStack-Native-Token': token } });
261
- return answer.status === 200 && answer.body?.data?.workspace_uid === workspaceUid && answer.body.data.protocol_version === 1;
262
- } catch { return false; }
263
- }
264
- async function stackbiteToken(stateDir) {
265
- for (const name of ['.stackbite_secret_token', '.office_secret_token']) {
266
- try {
267
- const token = (await readSmall(path.join(stateDir, name), 4096)).toString('utf8').trim();
268
- if (token && /^[\x20-\x7e]+$/.test(token)) return token;
269
- } catch (error) { if (error.code !== 'ENOENT') return null; }
270
- }
271
- return null;
272
- }
273
- async function stackbiteIdentity(url, stateDir, backendId, { timeout = 5000 } = {}) {
274
- const deadline = performance.now() + finiteTimeout(timeout, 5000);
275
- if (typeof url !== 'string' || !LOOPBACK.test(url)) return false;
276
- try {
277
- const token = await beforeDeadline(() => stackbiteToken(stateDir), deadline, 'token read timed out');
278
- if (!token) return false;
279
- if (!remaining(deadline)) return false;
280
- const answer = await request(new URL('/api/backend/status', url), { timeout: remaining(deadline), headers: { 'X-Office-Token': token } });
281
- return answer.status === 200 && answer.body?.status === 'ok' && answer.body.backend_id === backendId;
282
- } catch { return false; }
283
- }
284
-
285
- async function freePort(excluded = RESERVED_PORTS) {
286
- for (let attempt = 0; attempt < 20; attempt++) {
287
- const port = await new Promise((resolve, reject) => {
288
- const server = net.createServer();
289
- server.once('error', reject);
290
- server.listen(0, '127.0.0.1', () => { const { port: chosen } = server.address(); server.close(() => resolve(chosen)); });
291
- });
292
- if (!excluded.has(port)) return port;
293
- }
294
- fail('No free loopback port is available');
295
- }
296
-
297
- function spawnDetached(command, args, { env, cwd, log, spawnImpl = spawn }) {
298
- const output = fsSync.openSync(log, 'a', 0o600);
299
- try {
300
- const [file, ...prefix] = command;
301
- const child = spawnImpl(file, [...prefix, ...args], { env, cwd, detached: true, windowsHide: true, shell: false,
302
- stdio: ['ignore', output, output] });
303
- const exited = new Promise(resolve => {
304
- child.once('exit', code => resolve({ code }));
305
- child.once('error', () => resolve({ code: null }));
306
- });
307
- child.unref();
308
- return { child, exited };
309
- } finally { fsSync.closeSync(output); }
310
- }
311
- async function waitFor(probe, exited, timeout, what, log, deadline = performance.now() + timeout) {
312
- let ended = false;
313
- exited.then(() => { ended = true; });
314
- const unknown = `${what} did not become ready in time; it was left running; startup outcome unknown, intent retained. See ${log}`;
315
- while (remaining(deadline)) {
316
- const value = await beforeDeadline(() => Promise.race([probe(remaining(deadline)), exited.then(() => {
317
- fail(`${what} exited during startup; see ${log}`);
318
- })]), deadline, unknown);
319
- if (value) return value;
320
- if (ended) fail(`${what} exited during startup; see ${log}`);
321
- await new Promise(resolve => setTimeout(resolve, Math.min(150, remaining(deadline))));
322
- }
323
- fail(unknown);
324
- }
325
- async function readyRecord(log) {
326
- let text;
327
- try { text = (await readSmall(log, 1024 * 1024)).toString('utf8'); } catch { return null; }
328
- for (const line of text.split(/\r?\n/)) {
329
- try {
330
- const value = JSON.parse(line);
331
- if (value?.state === 'ready') return value;
332
- } catch { /* not the ready line */ }
333
- }
334
- return null;
335
- }
336
-
337
- // Startup intent (private token, launch id, port) is saved before any child is
338
- // spawned, so an attempt that timed out or crashed is never followed by a second
339
- // manager on the same state: a later open either authenticates that same attempt
340
- // or refuses. Proven previous-boot records are archived; nothing is stopped automatically.
341
- const RECOVERY = 'WorkBite never starts a second one on the same state. If it is still starting, open again shortly; '
342
- + 'if you have confirmed no process from this state is running, move that record aside and open again.';
343
- const LAUNCH = /^[0-9a-f]{16}$/;
344
- // Distinct from every JSON value (including null): only an absent file is "missing".
345
- const MISSING = Symbol('missing record');
346
- async function savedRecord(file, what) {
347
- try {
348
- const bytes = await readSmall(file, 65536);
349
- return { record: JSON.parse(bytes.toString('utf8')), bytes };
350
- } catch (error) {
351
- if (error.code === 'ENOENT') return MISSING;
352
- fail(`The ${what} record ${file} is unreadable; WorkBite does not replace it. ${RECOVERY}`);
353
- }
354
- }
355
- const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value)
356
- && (value.state === 'starting' || value.state === 'ready') && typeof value.launch === 'string' && LAUNCH.test(value.launch);
357
- const recordBytes = value => Buffer.from(JSON.stringify(value, null, 2) + '\n');
358
-
359
- // Called under open.lock. Compare the exact bytes, retain a private backup before
360
- // renaming the record, and refuse if it changed. Logs and native intent stay put.
361
- async function archivePreviousRecord(file, saved, original, boot) {
362
- if (!previousBoot(saved.boot, boot)) return false;
363
- const current = await readSmall(file, 65536);
364
- if (!current.equals(original)) fail('Startup record changed during recovery; refusing another launch.');
365
- const backup = `${file}.previous-boot-${randomBytes(12).toString('hex')}`;
366
- await fs.copyFile(file, backup, fsSync.constants.COPYFILE_EXCL);
367
- if (!(await readSmall(backup, 65536)).equals(original)
368
- || !(await readSmall(file, 65536)).equals(original)) {
369
- fail('Startup record changed during recovery; backup retained, launch refused.');
370
- }
371
- // The rename retains the original record's ACL. A separate backup survives a
372
- // crash before the new current-boot intent is persisted.
373
- await fs.rename(file, `${backup}.record`);
374
- if (!(await readSmall(`${backup}.record`, 65536)).equals(original)) {
375
- fail('Startup record changed during quarantine; archived evidence retained, launch refused.');
376
- }
377
- return true;
378
- }
379
- function checkBoot(saved, recordPath, boot) {
380
- if (Object.hasOwn(saved, 'boot') && !validBoot(saved.boot)) {
381
- fail(`The startup boot provenance in ${recordPath} is invalid; WorkBite does not replace it.`);
382
- }
383
- if (validBoot(saved.boot) && validBoot(boot) && saved.boot.host !== boot.host) {
384
- fail(`The startup record ${recordPath} belongs to a foreign host; its process state is unknown and could not be authenticated locally. `
385
- + 'No local endpoint or PID was probed; the record was retained. ' + RECOVERY);
386
- }
387
- }
388
- // A v2 (execution UI) bundle reuses only processes its own startup intent marked: the owner needs
389
- // this bundle's execution commands, the StackBite backend its native launch opt-in (read once at
390
- // startup). A matching token or UID alone is not that proof. Checked from the record before any
391
- // network contact; the record is kept and nothing is stopped. A v1 bundle never checks or writes it.
392
- const HANDOVER = 'Operator handover needed: WorkBite never stops, adopts or replaces it. Close the UIs using it, '
393
- + 'stop that process yourself (its pid is in the record), move the record aside and open again; WorkBite then '
394
- + 'starts a new one from this bundle.';
395
- const shown = value => (typeof value === 'string' ? JSON.stringify(value.slice(0, 40)) : value === undefined ? 'absent' : 'invalid');
396
- function requireExecutionMarker(saved, execution, recordPath, what, extra = {}) {
397
- if (!execution) return;
398
- const missing = saved.execution_bundle_version !== execution.bundleVersion
399
- || Object.entries(extra).some(([key, value]) => saved[key] !== value);
400
- if (missing) {
401
- fail(`The ${what} recorded in ${recordPath} was not started by the WorkBite ${execution.bundleVersion} execution bundle `
402
- + `(execution_bundle_version ${shown(saved.execution_bundle_version)}`
403
- + Object.keys(extra).map(key => `, ${key} ${saved[key] === undefined ? 'absent' : JSON.stringify(saved[key])}`).join('')
404
- + `), so it cannot serve 9 Execution. ${HANDOVER}`);
405
- }
406
- }
407
- const executionFields = (execution, extra = {}) => (execution ? { execution_bundle_version: execution.bundleVersion, ...extra } : {});
408
- const NATIVE_LAUNCH = { native_launch: true };
409
-
410
- async function ensureEngine({ commands, workspace, uid, layout, environment, spawnImpl, timeout = 60000, execution = null, boot = null, identityImpl = engineIdentity }) {
411
- timeout = finiteTimeout(timeout, 60000);
412
- const key = workspaceKey(workspace);
413
- const recordPath = path.join(layout.owners, `${key}.json`);
414
- const logFor = launch => path.join(layout.logs, `workstack-engine-${key}-${launch}.log`);
415
- const snapshot = await savedRecord(recordPath, 'Work Stack owner');
416
- const saved = snapshot === MISSING ? MISSING : snapshot.record;
417
- const original = snapshot === MISSING ? null : snapshot.bytes;
418
- if (saved !== MISSING) {
419
- if (!isRecord(saved) || saved.schema !== OWNER_SCHEMA || typeof saved.workspace !== 'string'
420
- || workspaceKey(saved.workspace) !== key || saved.workspace_uid !== uid
421
- || typeof saved.token !== 'string' || !/^[0-9a-f]{64}$/.test(saved.token)
422
- || (saved.state === 'ready' && (typeof saved.endpoint !== 'string' || !LOOPBACK.test(saved.endpoint)))) {
423
- fail(`The Work Stack owner record ${recordPath} is invalid for this workspace; WorkBite does not replace it. ${RECOVERY}`);
424
- }
425
- checkBoot(saved, recordPath, boot);
426
- requireExecutionMarker(saved, execution, recordPath, 'Work Stack owner');
427
- let endpoint = saved.state === 'ready' ? saved.endpoint : null;
428
- if (saved.state === 'starting') {
429
- const ready = await readyRecord(logFor(saved.launch));
430
- if (ready?.workspace_uid === uid && ready.protocol_version === 1) endpoint = ready.endpoint;
431
- }
432
- if (!await beforeDeadline(() => identityImpl(endpoint, saved.token, uid, { timeout: Math.min(5000, timeout) }),
433
- performance.now() + timeout, `Work Stack owner authentication timed out; outcome unknown; record retained: ${recordPath}; logs: ${layout.logs}`)) {
434
- if (!await archivePreviousRecord(recordPath, saved, original, boot))
435
- fail(`The Work Stack owner recorded in ${recordPath} could not be authenticated (still starting, or stopped). ${RECOVERY}`);
436
- } else {
437
- if (saved.state === 'starting' || validBoot(boot))
438
- await writeAtomic(recordPath, recordBytes({ ...saved, ...(validBoot(boot) ? { boot } : {}), state: 'ready', endpoint }));
439
- return { endpoint, reused: true, pid: saved.pid };
440
- }
441
- }
442
- if (!validBoot(boot)) fail('Trusted OS boot provenance is unavailable; no owner was started.');
443
- const intent = { boot, schema: OWNER_SCHEMA, state: 'starting', workspace, workspace_uid: uid,
444
- launch: randomBytes(8).toString('hex'), token: randomBytes(32).toString('hex'), ...executionFields(execution) };
445
- const log = logFor(intent.launch);
446
- await writeAtomic(recordPath, recordBytes(intent));
447
- const { child, exited } = spawnDetached(commands.workstack_engine,
448
- ['serve', '--workspace', workspace, '--workspace-uid', uid, '--host', '127.0.0.1', '--port', '0', '--native-runtime-control'],
449
- { env: { ...environment, WORKSTACK_NATIVE_RUNTIME_TOKEN: intent.token }, cwd: layout.root, log, spawnImpl });
450
- await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid }));
451
- const deadline = performance.now() + timeout;
452
- const ready = await waitFor(() => readyRecord(log), exited, timeout, 'Work Stack native owner', log, deadline);
453
- if (ready.workspace_uid !== uid || ready.protocol_version !== 1 || !LOOPBACK.test(ready.endpoint)
454
- || !await beforeDeadline(() => identityImpl(ready.endpoint, intent.token, uid, { timeout: Math.min(5000, remaining(deadline)) }),
455
- deadline, `Work Stack native owner identity timed out; outcome unknown; intent retained: ${recordPath}; see ${log}`))
456
- fail(`Work Stack native owner identity was not proven; intent retained: ${recordPath}; see ${log}`);
457
- await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid, state: 'ready', endpoint: ready.endpoint }));
458
- return { endpoint: ready.endpoint, reused: false, pid: child.pid };
459
- }
460
-
461
- async function ensureStackbite({ commands, layout, backendId, environment, spawnImpl, timeout = 120000, execution = null, boot = null, identityImpl = stackbiteIdentity }) {
462
- timeout = finiteTimeout(timeout, 120000);
463
- const recordPath = layout.stackbiteRecord;
464
- const logFor = launch => path.join(layout.logs, `stackbite-backend-${launch}.log`);
465
- const snapshot = await savedRecord(recordPath, 'StackBite backend');
466
- const saved = snapshot === MISSING ? MISSING : snapshot.record;
467
- const original = snapshot === MISSING ? null : snapshot.bytes;
468
- if (saved !== MISSING) {
469
- if (!isRecord(saved) || saved.schema !== STACKBITE_SCHEMA || saved.backend_id !== backendId
470
- || typeof saved.url !== 'string' || LOOPBACK.exec(saved.url)?.[1] !== String(saved.port)) {
471
- fail(`The StackBite backend record ${recordPath} is invalid for this state; WorkBite does not replace it. ${RECOVERY}`);
472
- }
473
- checkBoot(saved, recordPath, boot);
474
- requireExecutionMarker(saved, execution, recordPath, 'StackBite backend', NATIVE_LAUNCH);
475
- if (!await beforeDeadline(() => identityImpl(saved.url, layout.stackbite, backendId, { timeout: Math.min(5000, timeout) }),
476
- performance.now() + timeout, `StackBite backend authentication timed out; outcome unknown; record retained: ${recordPath}; logs: ${layout.logs}`)) {
477
- if (!await archivePreviousRecord(recordPath, saved, original, boot))
478
- fail(`The StackBite backend recorded in ${recordPath} could not be authenticated (still starting, or stopped). ${RECOVERY}`);
479
- } else {
480
- if (saved.state === 'starting' || validBoot(boot))
481
- await writeAtomic(recordPath, recordBytes({ ...saved, ...(validBoot(boot) ? { boot } : {}), state: 'ready' }));
482
- return { url: saved.url, reused: true, pid: saved.pid };
483
- }
484
- }
485
- if (!validBoot(boot)) fail('Trusted OS boot provenance is unavailable; no backend was started.');
486
- const port = await freePort();
487
- const url = `http://127.0.0.1:${port}`;
488
- const intent = { boot, schema: STACKBITE_SCHEMA, state: 'starting', backend_id: backendId, launch: randomBytes(8).toString('hex'), url, port,
489
- ...executionFields(execution, NATIVE_LAUNCH) };
490
- const log = logFor(intent.launch);
491
- const home = path.dirname(commands.stackbite[commands.stackbite.length - 1]);
492
- await writeAtomic(recordPath, recordBytes(intent));
493
- // The private Python terminal core and this backend's own loopback bridge are
494
- // mandatory for the room backend (taskrooms reads STACKBITE_BRIDGE_URL; the agent
495
- // CLI and frontend read STACKBITE_ROOM_BRIDGE_URL); caller values are overridden here only.
496
- // A v2 bundle also starts it with the WorkBite native launch opt-in (cleanEnvironment drops any
497
- // caller value, so a v1 backend never gets it); STACKBITE_HOME lets it resolve the bundle's native assets.
498
- const { child, exited } = spawnDetached(commands.stackbite, ['--room-backend', '--port', String(port)], {
499
- env: withPath({ ...environment, STACKBITE_HOME: home, STACKBITE_STATE_DIR: layout.stackbite,
500
- STACKBITE_PTY_BACKEND: 'private', STACKBITE_ROOM_BACKEND: '1', STACKBITE_TERMINAL_CORE: 'python',
501
- STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, PYINSTALLER_RESET_ENVIRONMENT: '1',
502
- ...(execution ? { STACKBITE_WORKBITE_NATIVE_LAUNCH: '1' } : {}) }, home),
503
- cwd: home, log, spawnImpl });
504
- await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid }));
505
- await waitFor(left => identityImpl(url, layout.stackbite, backendId, { timeout: Math.min(5000, left) }),
506
- exited, timeout, 'StackBite backend', log);
507
- await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid, state: 'ready' }));
508
- return { url, reused: false, pid: child.pid };
509
- }
510
-
511
- // v2 only, after this backend's identity is proven: one headless `stackbite --workbite-native-init`
512
- // selects (or, through StackBite's own durable coordinator, reuses) the native terminal daemon for
513
- // this backend. No UI, window, Room or Seat; exactly one exit-0 selected record is accepted.
514
- // Anything else (refused, Unknown, malformed, timed out) refuses the open: WorkBite never retries,
515
- // starts another daemon or falls back, and a later open asks the same coordinator again.
516
- const NATIVE_INIT_SCHEMA = 'stackbite.workbite-native-init.v1';
517
- async function initializeNativeRuntime({ commands, layout, url, environment, execImpl = execFile, timeout = 120000 }) {
518
- const home = path.dirname(commands.stackbite[commands.stackbite.length - 1]);
519
- const [file, ...prefix] = commands.stackbite;
520
- const result = await runOnce(file, [...prefix, '--workbite-native-init'],
521
- withPath({ ...environment, STACKBITE_HOME: home, STACKBITE_STATE_DIR: layout.stackbite,
522
- STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, STACKBITE_WORKBITE_NATIVE_LAUNCH: '1',
523
- PYINSTALLER_RESET_ENVIRONMENT: '1' }, home),
524
- timeout, execImpl, { cwd: home, maxBuffer: 64 * 1024 });
525
- const text = result.stdout.trim();
526
- let record = null;
527
- if (text && !/[\r\n]/.test(text) && text.length <= 4096) { try { record = JSON.parse(text); } catch { record = null; } }
528
- if (result.error || record === null || typeof record !== 'object' || Array.isArray(record) || Object.keys(record).length !== 2
529
- || record.schema !== NATIVE_INIT_SCHEMA || record.status !== 'selected') {
530
- const exit = result.error ? (result.error.killed ? 'timed out' : `exit ${result.error.code ?? 'unknown'}`) : 'no selected record';
531
- fail(`StackBite native runtime initialization did not select a native runtime (${exit}: ${boundedText(result.stderr) || 'no reason'}); `
532
- + 'the execution UI was not started. WorkBite does not retry, start another daemon or fall back; '
533
- + 'if the outcome is unknown, open again later and StackBite looks the same selection up.');
534
- }
535
- return record.status;
536
- }
537
-
538
- async function writeDockConfig(layout, url) {
539
- const bytes = Buffer.from(JSON.stringify({ schema: CONNECTION_SCHEMA, base_url: url, state_dir: layout.stackbite }, null, 2) + '\n');
540
- let current = null;
541
- try { current = await readSmall(layout.dock, 65536); } catch { current = null; }
542
- if (!current || !current.equals(bytes)) await writeAtomic(layout.dock, bytes);
543
- }
544
-
545
- // WB-EXEC-UI-V1 §4.4 device opt-in for 9 Execution, in WorkBite's own private settings file.
546
- // Written only when no execution_ui choice exists; a present choice (enabled or disabled, any
547
- // prefix) is kept byte for byte, and anything else there is refused, never repaired.
548
- const EXECUTION_UI_SCHEMA = 'workbite.execution-ui-settings.v1';
549
- const EXECUTION_UI_DEFAULT = Object.freeze({ schema: EXECUTION_UI_SCHEMA, enabled: true, prefix: 'ctrl+]', last_view_execution: false });
550
- const SETTINGS_FIX = 'WorkBite does not repair or replace it; fix it or move it aside and open again.';
551
- function validExecutionUi(value) {
552
- return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 4
553
- && value.schema === EXECUTION_UI_SCHEMA && typeof value.enabled === 'boolean' && typeof value.prefix === 'string'
554
- && typeof value.last_view_execution === 'boolean';
555
- }
556
- // Same-directory temporary file published by hard link: never replaces a file another writer created.
557
- async function publishNew(file, bytes) {
558
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomBytes(8).toString('hex')}.tmp`);
559
- const handle = await fs.open(temporary, 'wx', 0o600);
560
- try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
561
- try { await fs.link(temporary, file); return true; } catch (error) {
562
- if (error.code !== 'EEXIST') throw error;
563
- return false;
564
- } finally { await fs.unlink(temporary); }
565
- }
566
- async function ensureExecutionSettings(file, attempt = 0) {
567
- let bytes = null;
568
- try { bytes = await readSmall(file, 1024 * 1024); } catch (error) {
569
- if (error.code !== 'ENOENT') fail(`The execution UI settings ${file} are unreadable. ${SETTINGS_FIX}`);
570
- }
571
- if (bytes === null) {
572
- const created = await publishNew(file, Buffer.from(JSON.stringify({ execution_ui: EXECUTION_UI_DEFAULT }, null, 2) + '\n'));
573
- if (created) return 'initialized';
574
- if (attempt) fail(`The execution UI settings ${file} keep changing while opening. ${SETTINGS_FIX}`);
575
- return ensureExecutionSettings(file, attempt + 1);
576
- }
577
- let document;
578
- try {
579
- const text = bytes.toString('utf8');
580
- document = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
581
- } catch { fail(`The execution UI settings ${file} are not valid JSON. ${SETTINGS_FIX}`); }
582
- if (document === null || typeof document !== 'object' || Array.isArray(document)) {
583
- fail(`The execution UI settings ${file} are not a JSON object. ${SETTINGS_FIX}`);
584
- }
585
- if (!Object.hasOwn(document, 'execution_ui')) {
586
- // Other settings are kept; only the missing opt-in is added.
587
- await writeAtomic(file, Buffer.from(JSON.stringify({ ...document, execution_ui: EXECUTION_UI_DEFAULT }, null, 2) + '\n'));
588
- return 'initialized';
589
- }
590
- if (!validExecutionUi(document.execution_ui)) {
591
- fail(`The execution UI settings ${file} hold an execution_ui value that is not the exact four-key opt-in object `
592
- + `(schema "${EXECUTION_UI_SCHEMA}", enabled, prefix, last_view_execution). ${SETTINGS_FIX}`);
593
- }
594
- return document.execution_ui.enabled ? 'enabled' : 'disabled';
595
- }
596
-
597
- // The TypeScript UI's same-window 9 Execution (docs/WORKBITE-TYPESCRIPT-EXECUTION-CONFIG.md): set only by
598
- // this launcher for a verified v2 bundle whose device opt-in is enabled. The bridge is the bundle's own
599
- // verified workbite_tui (run by the UI with its fixed --workbite-execution-bridge argv), never a caller,
600
- // receipt or PATH value; the UI keeps its own WORKSTACK_TUI_SETTINGS, so these names carry the private
601
- // execution settings and intent paths separately. cleanEnvironment drops any caller WORKBITE_ value first.
602
- function typescriptExecutionEnvironment(layout, bridge) {
603
- return { WORKBITE_EXECUTION_UI: '1', WORKBITE_EXECUTION_BRIDGE: bridge,
604
- WORKBITE_EXECUTION_SETTINGS: layout.tuiSettings, WORKBITE_EXECUTION_INTENT: layout.tuiIntent };
605
- }
606
- const verifiedPath = value => typeof value === 'string' && path.isAbsolute(value) && !/[\x00-\x1f]/.test(value);
607
-
608
- function uiEnvironment(base, { executables, layout, endpoint, uid, backendId, execution = null, bridge = null, mobile = null }) {
609
- const home = path.dirname(executables.stackbite);
610
- return withPath({ ...base, WORKSTACK_TRANSPORT: 'native', WORKSTACK_NATIVE_CLIENT: executables.workstack_client,
611
- WORKSTACK_ENDPOINT: endpoint, WORKSTACK_WORKSPACE_UID: uid, WORKSTACK_STATE_DIR: layout.tuiState,
612
- WORKSTACK_EXECUTION_DOCK_CONFIG: layout.dock, WORKSTACK_EXECUTION_DOCK_BACKEND_ID: backendId,
613
- WORKSTACK_DEPLOY_JOURNAL_DIR: layout.journal, WORKSTACK_STACKBITE_EXECUTABLE: executables.stackbite,
614
- ...(execution ? { WORKSTACK_TUI_STATE: layout.tuiIntent, WORKSTACK_TUI_SETTINGS: layout.tuiSettings } : {}),
615
- ...(bridge ? typescriptExecutionEnvironment(layout, bridge) : {}),
616
- ...(mobile ? { WORKBITE_MOBILE_EXECUTABLE: executables.workbite_mobile,
617
- WORKBITE_MOBILE_HOST: JSON.stringify(mobile) } : {}) }, home);
618
- }
619
-
620
- function runForeground(command, args, environment, { spawnImpl = spawn, signalSource = process,
621
- timeout = 30000, onStarted = () => {} } = {}) {
622
- return new Promise((resolve, reject) => {
623
- const [file, ...prefix] = command;
624
- const child = spawnImpl(file, [...prefix, ...args], { env: environment, stdio: 'inherit', shell: false });
625
- let settled = false;
626
- const handlers = new Map();
627
- for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
628
- const handler = () => child.kill(signal);
629
- handlers.set(signal, handler);
630
- signalSource.on(signal, handler);
631
- }
632
- const timer = setTimeout(() => {
633
- settled = true;
634
- cleanup();
635
- reject(new Error('Work Stack terminal UI launch timed out; outcome unknown. No retry or fallback was attempted.'));
636
- }, finiteTimeout(timeout, 30000));
637
- const cleanup = () => {
638
- clearTimeout(timer);
639
- for (const [signal, handler] of handlers) signalSource.removeListener(signal, handler);
640
- };
641
- // Only launch acknowledgement is bounded. A successfully spawned UI runs until it exits.
642
- child.once('spawn', () => { if (!settled) { clearTimeout(timer); onStarted(); } });
643
- child.once('error', () => { settled = true; cleanup(); reject(new Error('Cannot start the verified Work Stack terminal UI')); });
644
- child.once('exit', (code, signal) => { settled = true; cleanup(); resolve({ code, signal }); });
645
- });
646
- }
647
-
648
- // commands maps each bundle executable to its argv prefix; tests substitute fixtures.
649
- async function openSuite({ installed, commands, workspace, init = false, stateRoot, environment = process.env,
650
- ui = true, frontend = 'typescript', spawnImpl = spawn, execImpl = execFile, platform = process.platform,
651
- stdout = process.stdout, stderr = process.stderr, timeouts = {}, bootImpl = bootIdentity, mobileBootstrap = undefined }) {
652
- if (!FRONTENDS.includes(frontend)) fail(`--frontend must be one of: ${FRONTENDS.join(', ')}`);
653
- if (workspace !== undefined) workspace = checkedWorkspace(workspace);
654
- else if (init) fail('--init needs --workspace ABS naming the new or empty folder');
655
- commands = commands || Object.fromEntries(Object.entries(installed.executables).map(([name, file]) => [name, [file]]));
656
- // A v2 bundle keeps its owner/backend markers and native init whichever frontend runs; only the
657
- // foreground UI differs. TypeScript is the default; rust needs a v2 bundle. There is no fallback between them.
658
- const execution = installed.bundle?.schema === BUNDLE_V2 ? { bundleVersion: installed.bundle.version } : null;
659
- if (frontend === 'rust' && !execution) {
660
- fail('--frontend rust needs a WorkBite execution (v2) bundle; this installed bundle has only the TypeScript UI. Nothing was started.');
661
- }
662
- const uiCommand = frontend === 'rust' ? 'workbite_tui' : 'workstack_tui';
663
- if (!(Array.isArray(commands[uiCommand]) && commands[uiCommand].length)) {
664
- fail(frontend === 'rust' ? 'This WorkBite bundle has no verified execution UI executable; nothing was started.'
665
- : 'This WorkBite bundle has no verified TypeScript UI executable; nothing was started.');
666
- }
667
- // A verified v2 bundle always holds the execution bridge; anything else is refused, never probed for.
668
- const bridge = execution && frontend === 'typescript' ? installed.executables?.workbite_tui : null;
669
- if (execution && frontend === 'typescript' && !verifiedPath(bridge)) {
670
- fail('This WorkBite execution bundle has no verified execution bridge executable; nothing was started.');
671
- }
672
- const layout = stateLayout(path.resolve(stateRoot));
673
- const stage = async (name, action) => {
674
- stderr.write(`[workbite] ${name}: starting\n`);
675
- try {
676
- const value = await action();
677
- stderr.write(`[workbite] ${name}: ready\n`);
678
- return value;
679
- } catch (error) {
680
- if (error.workbiteStage) throw error;
681
- stderr.write(`[workbite] ${name}: failed\n`);
682
- const wrapped = new Error(`WorkBite startup stage "${name}" failed: ${error.message}; state: ${layout.root}; logs: ${layout.logs}`,
683
- { cause: error });
684
- wrapped.workbiteStage = name;
685
- throw wrapped;
686
- }
687
- };
688
- // Fixed stage names only: never print child environment, tokens or native stdout.
689
- await stage('state preparation', () => prepareState(layout));
690
- const base = cleanEnvironment(environment);
691
- const startupExec = (file, args, options, callback) => {
692
- runOnce(file, args, options.env, options.timeout, execImpl, options)
693
- .then(result => callback(result.error, result.stdout,
694
- result.stderr || (result.error?.killed ? 'startup command timed out; outcome unknown' : '')));
695
- };
696
- if (execution) {
697
- // The execution UI's intent parent and deploy journal, made private (Windows DACL) before any file in them is written.
698
- await stage('private execution state', async () => {
699
- await safeDirectory(layout.nativeTui);
700
- await protectPrivateDirectories([layout.nativeTui, layout.journal], { platform, environment: base,
701
- execImpl: startupExec, timeout: finiteTimeout(timeouts.privateState, 60000) });
702
- });
703
- }
704
- const engineEnvironment = { ...base, WORK_STACK_RUNTIME: layout.workstackRuntime };
705
- const session = await stage('startup lock', () => withLock(layout.lock, async () => {
706
- let remembered = null;
707
- if (workspace === undefined) {
708
- remembered = await stage('workspace selection', () => rememberedWorkspace(layout));
709
- workspace = checkedWorkspace(remembered.workspace);
710
- }
711
- // Settings are settled before any workspace init or backend start, so a refused file changes nothing else.
712
- const executionUi = execution ? await stage('execution settings', () => ensureExecutionSettings(layout.tuiSettings)) : null;
713
- const uid = await stage(init ? 'workspace initialization' : 'workspace identity', () => init
714
- ? initializeWorkspace(commands, workspace, engineEnvironment, { timeout: timeouts.workspaceInit }) : readWorkspaceUid(workspace));
715
- if (remembered && uid !== remembered.uid) fail(`The workspace ${workspace} changed while opening. ${EXPLICIT}`);
716
- const backendId = await stage('backend identity', () => ensureBackendId(layout));
717
- const boot = await stage('host boot provenance', () => beforeDeadline(() => bootImpl({ platform, environment: base }),
718
- performance.now() + finiteTimeout(timeouts.boot, 10000), 'OS boot provenance timed out; no backend was started'));
719
- const engine = await stage('Work Stack owner', () => ensureEngine({ commands, workspace, uid, layout, environment: engineEnvironment, spawnImpl,
720
- timeout: timeouts.engine, execution, boot }));
721
- const stackbite = await stage('StackBite backend', () => ensureStackbite({ commands, layout, backendId, environment: base, spawnImpl,
722
- timeout: timeouts.stackbite, execution, boot }));
723
- if (execution) {
724
- await stage('native runtime selection', () => initializeNativeRuntime({ commands, layout, url: stackbite.url, environment: base, execImpl,
725
- timeout: timeouts.nativeInit }));
726
- }
727
- await stage('execution connection', () => writeDockConfig(layout, stackbite.url));
728
- // Only after the owner, StackBite and dock config are proven ready, and before the UI.
729
- await stage('workspace selection save', () => rememberWorkspace(layout, workspace, uid));
730
- return { uid, backendId, engine, stackbite, executionUi };
731
- }));
732
- const summary = { schema: 'workbite.open.v1', workspace, workspace_uid: session.uid, endpoint: session.engine.endpoint,
733
- owner_reused: session.engine.reused, stackbite_url: session.stackbite.url, stackbite_reused: session.stackbite.reused,
734
- backend_id: session.backendId, state: layout.root, frontend,
735
- ...(execution ? { execution_ui: session.executionUi, execution_bundle_version: execution.bundleVersion } : {}) };
736
- if (!ui) {
737
- stdout.write(JSON.stringify(summary) + '\n');
738
- return { code: 0, signal: null, summary };
739
- }
740
- const environmentForUi = uiEnvironment(base, { executables: installed.executables, layout, endpoint: session.engine.endpoint,
741
- uid: session.uid, backendId: session.backendId, execution: frontend === 'rust' ? execution : null,
742
- bridge: bridge && ['enabled', 'initialized'].includes(session.executionUi) ? bridge : null });
743
- if (frontend === 'typescript' && platform === 'win32' && verifiedPath(installed.executables?.workbite_mobile)) {
744
- const mobileRoot = path.join(layout.root, 'mobile');
745
- await stage('private phone state', async () => {
746
- await safeDirectory(mobileRoot);
747
- await protectPrivateDirectories([mobileRoot], { platform, environment: base, execImpl: startupExec,
748
- timeout: finiteTimeout(timeouts.privateState, 60000) });
749
- });
750
- environmentForUi.WORKBITE_MOBILE_EXECUTABLE = installed.executables.workbite_mobile;
751
- environmentForUi.WORKBITE_MOBILE_HOST = JSON.stringify({
752
- directory: path.join(mobileRoot, workspaceKey(workspace)), executable: installed.executables.workbite_mobile,
753
- scope: { host_id: session.backendId, workspace_uid: session.uid },
754
- native: { executable: installed.executables.workstack_client, cwd: workspace, endpoint: session.engine.endpoint,
755
- workspaceUid: session.uid, deployment: { connection: layout.dock, backendId: session.backendId, journalDir: layout.journal } },
756
- // Embedding hosts may supply the private operator invitation at first setup; never a token.
757
- ...(mobileBootstrap ? { initialInput: mobileBootstrap } : {}),
758
- });
759
- }
760
- // Exactly one foreground child in this terminal (inherited stdio, never detached or a new console).
761
- // The TypeScript UI gets the v1 environment (keeping its own familiar settings file) plus, when the
762
- // v2 opt-in is enabled, only the WORKBITE_EXECUTION_* names above.
763
- stderr.write('[workbite] terminal UI launch: starting\n');
764
- const result = await runForeground(commands[uiCommand], [], environmentForUi, { spawnImpl, timeout: timeouts.uiLaunch,
765
- onStarted: () => stderr.write('[workbite] terminal UI launch: ready\n') }).catch(error => {
766
- stderr.write('[workbite] terminal UI launch: failed\n');
767
- throw new Error(`WorkBite startup stage "terminal UI launch" failed: ${error.message}; state: ${layout.root}; logs: ${layout.logs}`, { cause: error });
768
- });
769
- return { ...result, summary };
770
- }
771
-
772
- module.exports = { cleanEnvironment, stateLayout, prepareState, ensureBackendId, workspaceKey, readWorkspaceUid,
773
- initializeWorkspace, engineIdentity, stackbiteIdentity, freePort, ensureEngine, ensureStackbite, writeDockConfig,
774
- withLock, uiEnvironment, openSuite, rememberedWorkspace, ensureExecutionSettings, initializeNativeRuntime, RESERVED_PORTS,
775
- SELECTION_SCHEMA, EXECUTION_UI_DEFAULT, NATIVE_INIT_SCHEMA, FRONTENDS, runForeground };
1
+ 'use strict';
2
+ // WorkBite suite supervisor: starts or reuses the verified Work Stack native
3
+ // owner and the StackBite room backend in WorkBite's own state, then runs the
4
+ // Work Stack terminal UI in the foreground. Backends outlive the UI so agents
5
+ // keep running; nothing here stops, adopts or kills a process it cannot
6
+ // authenticate as its own.
7
+ const fs = require('node:fs/promises');
8
+ const fsSync = require('node:fs');
9
+ const http = require('node:http');
10
+ const net = require('node:net');
11
+ const path = require('node:path');
12
+ const { performance } = require('node:perf_hooks');
13
+ const { spawn, execFile } = require('node:child_process');
14
+ const { randomBytes, randomUUID } = require('node:crypto');
15
+ const { fail, exists, safeDirectory, writeAtomic, readJson, readSmall, sha256, UUID, BACKEND_ID } = require('./common');
16
+ const { bootIdentity, validBoot, previousBoot } = require('./boot-provenance');
17
+ const { withLock } = require('./startup-lock');
18
+ const { BUNDLE_V2 } = require('./install');
19
+ const { protectPrivateDirectories } = require('./private-state');
20
+
21
+ const BACKEND_SCHEMA = 'stackbite.workstack-backend.v1';
22
+ const CONNECTION_SCHEMA = 'workstack.helm-connection.v1';
23
+ const OWNER_SCHEMA = 'workbite.owner.v2';
24
+ const STACKBITE_SCHEMA = 'workbite.stackbite-backend.v2';
25
+ const SELECTION_SCHEMA = 'workbite.workspace-selection.v1';
26
+ // Foreground UI per open: the familiar TypeScript Work Stack UI by default, the unified Rust UI on request.
27
+ const FRONTENDS = Object.freeze(['typescript', 'rust']);
28
+ const LOOPBACK = /^http:\/\/127\.0\.0\.1:([1-9][0-9]{0,4})$/;
29
+ // Ports of the existing official StackBite and its previews are never chosen.
30
+ const RESERVED_PORTS = new Set([8089, 8090, 8091, 54036, 54039, 54236, 54239]);
31
+ // Display preferences a child may keep (as Work Stack's viewer environment does);
32
+ // every other STACKBITE_/HELM_/OFFICE_ variable is caller Room/Seat routing or auth.
33
+ const PREFERENCES = new Set(['STACKBITE_CHROME_PROFILE', 'STACKBITE_CLIPBOARD_OSC52', 'STACKBITE_FRONTEND',
34
+ 'STACKBITE_SCROLLBACK_LINES', 'STACKBITE_TERMINAL_CORE', 'STACKBITE_UPDATE_CHECK']);
35
+
36
+ function cleanEnvironment(source = process.env) {
37
+ const result = {};
38
+ for (const [key, value] of Object.entries(source)) {
39
+ const upper = key.toUpperCase();
40
+ if (!key || value === undefined) continue;
41
+ if (/^(STACKBITE_|HELM_|OFFICE_)/.test(upper) && !PREFERENCES.has(upper)) continue;
42
+ if (/^(WORKSTACK_|WORK_STACK_|WORKBITE_)/.test(upper) || upper === 'PYINSTALLER_RESET_ENVIRONMENT') continue;
43
+ result[key] = value;
44
+ }
45
+ return result;
46
+ }
47
+ function withPath(environment, directory) {
48
+ const key = Object.keys(environment).find(name => name.toUpperCase() === 'PATH') || 'PATH';
49
+ const current = environment[key];
50
+ return { ...environment, [key]: current ? directory + path.delimiter + current : directory };
51
+ }
52
+
53
+ function stateLayout(root) {
54
+ return { root, stackbite: path.join(root, 'stackbite'), workstackRuntime: path.join(root, 'workstack-runtime'),
55
+ journal: path.join(root, 'deploy-journal'), tuiState: path.join(root, 'workstack-tui'), owners: path.join(root, 'owners'),
56
+ logs: path.join(root, 'logs'), dock: path.join(root, 'execution-dock.json'),
57
+ stackbiteRecord: path.join(root, 'stackbite-backend.json'), lock: path.join(root, 'open.lock'),
58
+ selection: path.join(root, 'last-workspace.json'),
59
+ // v2 execution UI only: its private intent journal and device settings, never under a workspace.
60
+ nativeTui: path.join(root, 'workstack-native-tui'),
61
+ tuiIntent: path.join(root, 'workstack-native-tui', 'intent.json'),
62
+ tuiSettings: path.join(root, 'workstack-native-tui', 'tui.json') };
63
+ }
64
+ async function prepareState(layout) {
65
+ for (const directory of [layout.root, layout.stackbite, layout.workstackRuntime, layout.journal, layout.tuiState,
66
+ layout.owners, layout.logs]) await safeDirectory(directory);
67
+ }
68
+
69
+ // The operator pin StackBite reads from <state>/workstack-backend.json; generated once, never replaced.
70
+ async function ensureBackendId(layout) {
71
+ const file = path.join(layout.stackbite, 'workstack-backend.json');
72
+ if (!await exists(file)) {
73
+ const backendId = `workbite-${randomBytes(12).toString('hex')}`;
74
+ const bytes = Buffer.from(JSON.stringify({ schema: BACKEND_SCHEMA, backend_id: backendId }) + '\n');
75
+ try {
76
+ const handle = await fs.open(file, 'wx', 0o600);
77
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
78
+ } catch (error) { if (error.code !== 'EEXIST') throw error; }
79
+ }
80
+ let document;
81
+ try { document = await readJson(file, 4096); } catch { fail('WorkBite StackBite backend identity file is unreadable; inspect it rather than replacing it.'); }
82
+ if (document === null || typeof document !== 'object' || Array.isArray(document) || Object.keys(document).length !== 2
83
+ || document.schema !== BACKEND_SCHEMA || typeof document.backend_id !== 'string' || !BACKEND_ID.test(document.backend_id)) {
84
+ fail('WorkBite StackBite backend identity file is invalid; inspect it rather than replacing it.');
85
+ }
86
+ return document.backend_id;
87
+ }
88
+
89
+ function workspaceKey(workspace, platform = process.platform) {
90
+ return sha256(Buffer.from(platform === 'win32' ? workspace.toLowerCase() : workspace)).slice(0, 24);
91
+ }
92
+ function checkedWorkspace(value) {
93
+ if (typeof value !== 'string' || !path.isAbsolute(value) || /[\x00-\x1f]/.test(value)
94
+ || (process.platform === 'win32' && /^[\\/](?![\\/])/.test(value))) fail('--workspace must be an absolute path');
95
+ return path.resolve(value);
96
+ }
97
+ // The genuine Work Stack descriptor: workspace.json "id". Nothing is fabricated or repaired.
98
+ async function readWorkspaceUid(workspace) {
99
+ const descriptor = path.join(workspace, 'workspace.json');
100
+ if (!await exists(descriptor)) fail('This folder is not a Work Stack workspace (no workspace.json). Use --init only for a new or empty folder.');
101
+ let document;
102
+ try { document = await readJson(descriptor, 1024 * 1024); } catch { fail('workspace.json is unreadable or not JSON; WorkBite does not repair it.'); }
103
+ const uid = document?.id;
104
+ if (typeof uid !== 'string' || !UUID.test(uid)) fail('workspace.json has no canonical workspace UUID id.');
105
+ return uid;
106
+ }
107
+
108
+ function boundedText(text) {
109
+ const line = String(text).split(/\r?\n/).map(value => value.trim()).filter(Boolean).pop() || '';
110
+ return line.replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 300);
111
+ }
112
+ const finiteTimeout = (value, fallback) => Number.isFinite(value) && value > 0 ? value : fallback;
113
+ const remaining = deadline => Math.max(0, deadline - performance.now());
114
+ // Bounds observation, not kernel filesystem I/O or the lifetime of an owned backend.
115
+ async function beforeDeadline(action, deadline, message) {
116
+ const left = remaining(deadline);
117
+ if (!left) throw new Error(message);
118
+ let timer;
119
+ try {
120
+ const result = await Promise.race([Promise.resolve().then(action), new Promise((_, reject) => {
121
+ timer = setTimeout(() => reject(new Error(message)), left);
122
+ })]);
123
+ if (!remaining(deadline)) throw new Error(message);
124
+ return result;
125
+ } finally { clearTimeout(timer); }
126
+ }
127
+ function runOnce(file, args, environment, timeout = 120000, execImpl = execFile, options = {}) {
128
+ timeout = finiteTimeout(timeout, 120000);
129
+ return new Promise(resolve => {
130
+ let child, settled = false;
131
+ const finish = (error, stdout, stderr) => {
132
+ if (settled) return;
133
+ settled = true;
134
+ clearTimeout(timer);
135
+ resolve({ error, stdout: stdout || '', stderr: stderr || '' });
136
+ };
137
+ const timer = setTimeout(() => {
138
+ // A kill request is not proof of exit. Refuse with an unknown outcome; never retry.
139
+ finish(Object.assign(new Error('startup command timed out; outcome unknown'), { killed: true, code: 'ETIMEDOUT' }));
140
+ try { child?.kill('SIGKILL'); } catch { /* outcome remains unknown */ }
141
+ }, timeout);
142
+ try {
143
+ child = execImpl(file, args, { ...options, env: environment, shell: false, windowsHide: true, timeout,
144
+ maxBuffer: options.maxBuffer || 1024 * 1024, encoding: 'utf8' }, finish);
145
+ } catch (error) { finish(error); }
146
+ });
147
+ }
148
+ async function initializeWorkspace(commands, workspace, environment, { timeout, execImpl } = {}) {
149
+ if (await exists(path.join(workspace, 'workspace.json'))) fail('This folder is already a Work Stack workspace; open it without --init.');
150
+ if (!await exists(workspace)) {
151
+ try { await fs.mkdir(workspace, { mode: 0o700 }); } catch { fail('Cannot create the workspace folder; its parent folder must exist.'); }
152
+ }
153
+ const uid = randomUUID();
154
+ const [file, ...prefix] = commands.workstack_engine;
155
+ const result = await runOnce(file, [...prefix, 'init', '--workspace', workspace, '--workspace-uid', uid], environment, timeout, execImpl);
156
+ let record;
157
+ try { record = JSON.parse(result.stdout.trim()); } catch { record = null; }
158
+ if (result.error || record?.state !== 'initialized' || record.workspace_uid !== uid) {
159
+ fail(`Work Stack init refused this folder: ${result.error?.killed ? 'timed out; outcome unknown; inspect the workspace before opening again' : boundedText(result.stderr) || 'no ready record'}`);
160
+ }
161
+ return uid;
162
+ }
163
+
164
+ // The workspace a bare `workbite` reopens: the last one opened successfully, or, for
165
+ // state from 0.1.0 with no saved selection, the single owner record it holds. It is
166
+ // never guessed (no CWD, mtime or default folder) and never silently replaced by
167
+ // another workspace; the genuine workspace.json id must still match.
168
+ const EXPLICIT = 'Open it once with: workbite open --workspace ABS';
169
+ async function currentUid(workspace, what) {
170
+ try { return await readWorkspaceUid(workspace); } catch (error) {
171
+ fail(`The ${what} ${workspace} is missing, moved or no longer a valid Work Stack workspace (${error.message}) ${EXPLICIT}`);
172
+ }
173
+ }
174
+ async function rememberedWorkspace(layout) {
175
+ let saved;
176
+ try { saved = await readJson(layout.selection, 8192); } catch (error) {
177
+ if (error.code !== 'ENOENT') fail(`The saved workspace selection ${layout.selection} is unreadable. ${EXPLICIT}`);
178
+ return ownerWorkspace(layout);
179
+ }
180
+ if (saved === null || typeof saved !== 'object' || Array.isArray(saved) || Object.keys(saved).length !== 3
181
+ || saved.schema !== SELECTION_SCHEMA || typeof saved.workspace !== 'string' || !path.isAbsolute(saved.workspace)
182
+ || typeof saved.workspace_uid !== 'string' || !UUID.test(saved.workspace_uid)) {
183
+ fail(`The saved workspace selection ${layout.selection} is invalid. ${EXPLICIT}`);
184
+ }
185
+ if (await currentUid(saved.workspace, 'last opened workspace') !== saved.workspace_uid) {
186
+ fail(`The last opened workspace ${saved.workspace} was replaced (its workspace.json id changed); WorkBite does not open it silently. ${EXPLICIT}`);
187
+ }
188
+ return { workspace: saved.workspace, uid: saved.workspace_uid };
189
+ }
190
+ async function ownerWorkspace(layout) {
191
+ let names = [];
192
+ try { names = (await fs.readdir(layout.owners)).filter(name => /^[0-9a-f]{24}\.json$/.test(name)); } catch (error) {
193
+ if (error.code !== 'ENOENT') fail(`WorkBite owner records are unreadable. ${EXPLICIT}`);
194
+ }
195
+ if (!names.length) fail(`No workspace has been opened yet. ${EXPLICIT} (add --init for a new or empty folder).`);
196
+ if (names.length > 1) fail(`WorkBite has ${names.length} workspace owner records and will not guess which one to open. ${EXPLICIT}`);
197
+ const file = path.join(layout.owners, names[0]);
198
+ let record;
199
+ try { record = await readJson(file); } catch { fail(`The Work Stack owner record ${file} is unreadable. ${EXPLICIT}`); }
200
+ if (!isRecord(record) || record.schema !== OWNER_SCHEMA || typeof record.workspace !== 'string' || !path.isAbsolute(record.workspace)
201
+ || `${workspaceKey(record.workspace)}.json` !== names[0] || typeof record.workspace_uid !== 'string' || !UUID.test(record.workspace_uid)) {
202
+ fail(`The Work Stack owner record ${file} is not a valid owner record. ${EXPLICIT}`);
203
+ }
204
+ if (await currentUid(record.workspace, 'workspace recorded in the owner record') !== record.workspace_uid) {
205
+ fail(`The workspace recorded in ${file} was replaced (its workspace.json id changed); WorkBite does not open it silently. ${EXPLICIT}`);
206
+ }
207
+ return { workspace: record.workspace, uid: record.workspace_uid };
208
+ }
209
+ async function rememberWorkspace(layout, workspace, uid) {
210
+ const bytes = Buffer.from(JSON.stringify({ schema: SELECTION_SCHEMA, workspace, workspace_uid: uid }, null, 2) + '\n');
211
+ let current = null;
212
+ try { current = await readSmall(layout.selection, 8192); } catch { current = null; }
213
+ if (!current || !current.equals(bytes)) await writeAtomic(layout.selection, bytes);
214
+ }
215
+
216
+ function request(url, { method = 'GET', headers = {}, body, timeout = 5000 } = {}) {
217
+ const match = LOOPBACK.exec(url.origin);
218
+ if (!match) return Promise.reject(new Error('not a loopback origin'));
219
+ return new Promise((resolve, reject) => {
220
+ timeout = finiteTimeout(timeout, 5000);
221
+ let timer, responseStream, settled = false;
222
+ const finish = (error, value) => {
223
+ if (settled) return;
224
+ settled = true;
225
+ clearTimeout(timer);
226
+ if (error) reject(error); else resolve(value);
227
+ };
228
+ const payload = body === undefined ? undefined : Buffer.from(JSON.stringify(body));
229
+ const req = http.request(url, { method, agent: false,
230
+ headers: { ...headers, ...(payload ? { 'Content-Type': 'application/json', 'Content-Length': payload.length } : {}) } },
231
+ response => {
232
+ responseStream = response;
233
+ const chunks = [];
234
+ let size = 0;
235
+ response.on('data', chunk => {
236
+ size += chunk.length;
237
+ if (size > 1024 * 1024) abort(new Error('response too large')); else chunks.push(chunk);
238
+ });
239
+ response.on('end', () => {
240
+ let parsed = null;
241
+ try { parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { parsed = null; }
242
+ finish(null, { status: response.statusCode, body: parsed });
243
+ });
244
+ response.on('error', error => finish(error));
245
+ response.on('aborted', () => finish(new Error('response aborted')));
246
+ response.on('close', () => { if (!response.complete) finish(new Error('response closed before completion')); });
247
+ });
248
+ const abort = error => { finish(error); responseStream?.destroy(); req.destroy(); };
249
+ // A socket inactivity timeout can be extended forever by trickled bytes.
250
+ timer = setTimeout(() => abort(new Error('request timed out')), timeout);
251
+ req.on('error', error => finish(error));
252
+ req.end(payload);
253
+ });
254
+ }
255
+
256
+ // Owner identity is proven only by the private lifecycle token this WorkBite generated.
257
+ async function engineIdentity(endpoint, token, workspaceUid, { timeout = 5000 } = {}) {
258
+ if (typeof endpoint !== 'string' || !LOOPBACK.test(endpoint) || typeof token !== 'string' || !token) return false;
259
+ try {
260
+ const answer = await request(new URL('/api/v1/native-runtime/identity', endpoint), { timeout, headers: { 'X-WorkStack-Native-Token': token } });
261
+ return answer.status === 200 && answer.body?.data?.workspace_uid === workspaceUid && answer.body.data.protocol_version === 1;
262
+ } catch { return false; }
263
+ }
264
+ async function stackbiteToken(stateDir) {
265
+ for (const name of ['.stackbite_secret_token', '.office_secret_token']) {
266
+ try {
267
+ const token = (await readSmall(path.join(stateDir, name), 4096)).toString('utf8').trim();
268
+ if (token && /^[\x20-\x7e]+$/.test(token)) return token;
269
+ } catch (error) { if (error.code !== 'ENOENT') return null; }
270
+ }
271
+ return null;
272
+ }
273
+ async function stackbiteIdentity(url, stateDir, backendId, { timeout = 5000 } = {}) {
274
+ const deadline = performance.now() + finiteTimeout(timeout, 5000);
275
+ if (typeof url !== 'string' || !LOOPBACK.test(url)) return false;
276
+ try {
277
+ const token = await beforeDeadline(() => stackbiteToken(stateDir), deadline, 'token read timed out');
278
+ if (!token) return false;
279
+ if (!remaining(deadline)) return false;
280
+ const answer = await request(new URL('/api/backend/status', url), { timeout: remaining(deadline), headers: { 'X-Office-Token': token } });
281
+ return answer.status === 200 && answer.body?.status === 'ok' && answer.body.backend_id === backendId;
282
+ } catch { return false; }
283
+ }
284
+
285
+ async function freePort(excluded = RESERVED_PORTS) {
286
+ for (let attempt = 0; attempt < 20; attempt++) {
287
+ const port = await new Promise((resolve, reject) => {
288
+ const server = net.createServer();
289
+ server.once('error', reject);
290
+ server.listen(0, '127.0.0.1', () => { const { port: chosen } = server.address(); server.close(() => resolve(chosen)); });
291
+ });
292
+ if (!excluded.has(port)) return port;
293
+ }
294
+ fail('No free loopback port is available');
295
+ }
296
+
297
+ function spawnDetached(command, args, { env, cwd, log, spawnImpl = spawn }) {
298
+ const output = fsSync.openSync(log, 'a', 0o600);
299
+ try {
300
+ const [file, ...prefix] = command;
301
+ const child = spawnImpl(file, [...prefix, ...args], { env, cwd, detached: true, windowsHide: true, shell: false,
302
+ stdio: ['ignore', output, output] });
303
+ const exited = new Promise(resolve => {
304
+ child.once('exit', code => resolve({ code }));
305
+ child.once('error', () => resolve({ code: null }));
306
+ });
307
+ child.unref();
308
+ return { child, exited };
309
+ } finally { fsSync.closeSync(output); }
310
+ }
311
+ async function waitFor(probe, exited, timeout, what, log, deadline = performance.now() + timeout) {
312
+ let ended = false;
313
+ exited.then(() => { ended = true; });
314
+ const unknown = `${what} did not become ready in time; it was left running; startup outcome unknown, intent retained. See ${log}`;
315
+ while (remaining(deadline)) {
316
+ const value = await beforeDeadline(() => Promise.race([probe(remaining(deadline)), exited.then(() => {
317
+ fail(`${what} exited during startup; see ${log}`);
318
+ })]), deadline, unknown);
319
+ if (value) return value;
320
+ if (ended) fail(`${what} exited during startup; see ${log}`);
321
+ await new Promise(resolve => setTimeout(resolve, Math.min(150, remaining(deadline))));
322
+ }
323
+ fail(unknown);
324
+ }
325
+ async function readyRecord(log) {
326
+ let text;
327
+ try { text = (await readSmall(log, 1024 * 1024)).toString('utf8'); } catch { return null; }
328
+ for (const line of text.split(/\r?\n/)) {
329
+ try {
330
+ const value = JSON.parse(line);
331
+ if (value?.state === 'ready') return value;
332
+ } catch { /* not the ready line */ }
333
+ }
334
+ return null;
335
+ }
336
+
337
+ // Startup intent (private token, launch id, port) is saved before any child is
338
+ // spawned, so an attempt that timed out or crashed is never followed by a second
339
+ // manager on the same state: a later open either authenticates that same attempt
340
+ // or refuses. Proven previous-boot records are archived; nothing is stopped automatically.
341
+ const RECOVERY = 'WorkBite never starts a second one on the same state. If it is still starting, open again shortly; '
342
+ + 'if you have confirmed no process from this state is running, move that record aside and open again.';
343
+ const LAUNCH = /^[0-9a-f]{16}$/;
344
+ // Distinct from every JSON value (including null): only an absent file is "missing".
345
+ const MISSING = Symbol('missing record');
346
+ async function savedRecord(file, what) {
347
+ try {
348
+ const bytes = await readSmall(file, 65536);
349
+ return { record: JSON.parse(bytes.toString('utf8')), bytes };
350
+ } catch (error) {
351
+ if (error.code === 'ENOENT') return MISSING;
352
+ fail(`The ${what} record ${file} is unreadable; WorkBite does not replace it. ${RECOVERY}`);
353
+ }
354
+ }
355
+ const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value)
356
+ && (value.state === 'starting' || value.state === 'ready') && typeof value.launch === 'string' && LAUNCH.test(value.launch);
357
+ const recordBytes = value => Buffer.from(JSON.stringify(value, null, 2) + '\n');
358
+
359
+ // Called under open.lock. Compare the exact bytes, retain a private backup before
360
+ // renaming the record, and refuse if it changed. Logs and native intent stay put.
361
+ async function archivePreviousRecord(file, saved, original, boot) {
362
+ if (!previousBoot(saved.boot, boot)) return false;
363
+ const current = await readSmall(file, 65536);
364
+ if (!current.equals(original)) fail('Startup record changed during recovery; refusing another launch.');
365
+ const backup = `${file}.previous-boot-${randomBytes(12).toString('hex')}`;
366
+ await fs.copyFile(file, backup, fsSync.constants.COPYFILE_EXCL);
367
+ if (!(await readSmall(backup, 65536)).equals(original)
368
+ || !(await readSmall(file, 65536)).equals(original)) {
369
+ fail('Startup record changed during recovery; backup retained, launch refused.');
370
+ }
371
+ // The rename retains the original record's ACL. A separate backup survives a
372
+ // crash before the new current-boot intent is persisted.
373
+ await fs.rename(file, `${backup}.record`);
374
+ if (!(await readSmall(`${backup}.record`, 65536)).equals(original)) {
375
+ fail('Startup record changed during quarantine; archived evidence retained, launch refused.');
376
+ }
377
+ return true;
378
+ }
379
+ function checkBoot(saved, recordPath, boot) {
380
+ if (Object.hasOwn(saved, 'boot') && !validBoot(saved.boot)) {
381
+ fail(`The startup boot provenance in ${recordPath} is invalid; WorkBite does not replace it.`);
382
+ }
383
+ if (validBoot(saved.boot) && validBoot(boot) && saved.boot.host !== boot.host) {
384
+ fail(`The startup record ${recordPath} belongs to a foreign host; its process state is unknown and could not be authenticated locally. `
385
+ + 'No local endpoint or PID was probed; the record was retained. ' + RECOVERY);
386
+ }
387
+ }
388
+ // A v2 (execution UI) bundle reuses only processes its own startup intent marked: the owner needs
389
+ // this bundle's execution commands, the StackBite backend its native launch opt-in (read once at
390
+ // startup). A matching token or UID alone is not that proof. Checked from the record before any
391
+ // network contact; the record is kept and nothing is stopped. A v1 bundle never checks or writes it.
392
+ const HANDOVER = 'Operator handover needed: WorkBite never stops, adopts or replaces it. Close the UIs using it, '
393
+ + 'stop that process yourself (its pid is in the record), move the record aside and open again; WorkBite then '
394
+ + 'starts a new one from this bundle.';
395
+ const shown = value => (typeof value === 'string' ? JSON.stringify(value.slice(0, 40)) : value === undefined ? 'absent' : 'invalid');
396
+ function requireExecutionMarker(saved, execution, recordPath, what, extra = {}) {
397
+ if (!execution) return;
398
+ const missing = saved.execution_bundle_version !== execution.bundleVersion
399
+ || Object.entries(extra).some(([key, value]) => saved[key] !== value);
400
+ if (missing) {
401
+ fail(`The ${what} recorded in ${recordPath} was not started by the WorkBite ${execution.bundleVersion} execution bundle `
402
+ + `(execution_bundle_version ${shown(saved.execution_bundle_version)}`
403
+ + Object.keys(extra).map(key => `, ${key} ${saved[key] === undefined ? 'absent' : JSON.stringify(saved[key])}`).join('')
404
+ + `), so it cannot serve 9 Execution. ${HANDOVER}`);
405
+ }
406
+ }
407
+ const executionFields = (execution, extra = {}) => (execution ? { execution_bundle_version: execution.bundleVersion, ...extra } : {});
408
+ const NATIVE_LAUNCH = { native_launch: true };
409
+
410
+ async function ensureEngine({ commands, workspace, uid, layout, environment, spawnImpl, timeout = 60000, execution = null, boot = null, identityImpl = engineIdentity }) {
411
+ timeout = finiteTimeout(timeout, 60000);
412
+ const key = workspaceKey(workspace);
413
+ const recordPath = path.join(layout.owners, `${key}.json`);
414
+ const logFor = launch => path.join(layout.logs, `workstack-engine-${key}-${launch}.log`);
415
+ const snapshot = await savedRecord(recordPath, 'Work Stack owner');
416
+ const saved = snapshot === MISSING ? MISSING : snapshot.record;
417
+ const original = snapshot === MISSING ? null : snapshot.bytes;
418
+ if (saved !== MISSING) {
419
+ if (!isRecord(saved) || saved.schema !== OWNER_SCHEMA || typeof saved.workspace !== 'string'
420
+ || workspaceKey(saved.workspace) !== key || saved.workspace_uid !== uid
421
+ || typeof saved.token !== 'string' || !/^[0-9a-f]{64}$/.test(saved.token)
422
+ || (saved.state === 'ready' && (typeof saved.endpoint !== 'string' || !LOOPBACK.test(saved.endpoint)))) {
423
+ fail(`The Work Stack owner record ${recordPath} is invalid for this workspace; WorkBite does not replace it. ${RECOVERY}`);
424
+ }
425
+ checkBoot(saved, recordPath, boot);
426
+ requireExecutionMarker(saved, execution, recordPath, 'Work Stack owner');
427
+ let endpoint = saved.state === 'ready' ? saved.endpoint : null;
428
+ if (saved.state === 'starting') {
429
+ const ready = await readyRecord(logFor(saved.launch));
430
+ if (ready?.workspace_uid === uid && ready.protocol_version === 1) endpoint = ready.endpoint;
431
+ }
432
+ if (!await beforeDeadline(() => identityImpl(endpoint, saved.token, uid, { timeout: Math.min(5000, timeout) }),
433
+ performance.now() + timeout, `Work Stack owner authentication timed out; outcome unknown; record retained: ${recordPath}; logs: ${layout.logs}`)) {
434
+ if (!await archivePreviousRecord(recordPath, saved, original, boot))
435
+ fail(`The Work Stack owner recorded in ${recordPath} could not be authenticated (still starting, or stopped). ${RECOVERY}`);
436
+ } else {
437
+ if (saved.state === 'starting' || validBoot(boot))
438
+ await writeAtomic(recordPath, recordBytes({ ...saved, ...(validBoot(boot) ? { boot } : {}), state: 'ready', endpoint }));
439
+ return { endpoint, reused: true, pid: saved.pid };
440
+ }
441
+ }
442
+ if (!validBoot(boot)) fail('Trusted OS boot provenance is unavailable; no owner was started.');
443
+ const intent = { boot, schema: OWNER_SCHEMA, state: 'starting', workspace, workspace_uid: uid,
444
+ launch: randomBytes(8).toString('hex'), token: randomBytes(32).toString('hex'), ...executionFields(execution) };
445
+ const log = logFor(intent.launch);
446
+ await writeAtomic(recordPath, recordBytes(intent));
447
+ const { child, exited } = spawnDetached(commands.workstack_engine,
448
+ ['serve', '--workspace', workspace, '--workspace-uid', uid, '--host', '127.0.0.1', '--port', '0', '--native-runtime-control'],
449
+ { env: { ...environment, WORKSTACK_NATIVE_RUNTIME_TOKEN: intent.token }, cwd: layout.root, log, spawnImpl });
450
+ await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid }));
451
+ const deadline = performance.now() + timeout;
452
+ const ready = await waitFor(() => readyRecord(log), exited, timeout, 'Work Stack native owner', log, deadline);
453
+ if (ready.workspace_uid !== uid || ready.protocol_version !== 1 || !LOOPBACK.test(ready.endpoint)
454
+ || !await beforeDeadline(() => identityImpl(ready.endpoint, intent.token, uid, { timeout: Math.min(5000, remaining(deadline)) }),
455
+ deadline, `Work Stack native owner identity timed out; outcome unknown; intent retained: ${recordPath}; see ${log}`))
456
+ fail(`Work Stack native owner identity was not proven; intent retained: ${recordPath}; see ${log}`);
457
+ await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid, state: 'ready', endpoint: ready.endpoint }));
458
+ return { endpoint: ready.endpoint, reused: false, pid: child.pid };
459
+ }
460
+
461
+ async function ensureStackbite({ commands, layout, backendId, environment, spawnImpl, timeout = 120000, execution = null, boot = null, identityImpl = stackbiteIdentity }) {
462
+ timeout = finiteTimeout(timeout, 120000);
463
+ const recordPath = layout.stackbiteRecord;
464
+ const logFor = launch => path.join(layout.logs, `stackbite-backend-${launch}.log`);
465
+ const snapshot = await savedRecord(recordPath, 'StackBite backend');
466
+ const saved = snapshot === MISSING ? MISSING : snapshot.record;
467
+ const original = snapshot === MISSING ? null : snapshot.bytes;
468
+ if (saved !== MISSING) {
469
+ if (!isRecord(saved) || saved.schema !== STACKBITE_SCHEMA || saved.backend_id !== backendId
470
+ || typeof saved.url !== 'string' || LOOPBACK.exec(saved.url)?.[1] !== String(saved.port)) {
471
+ fail(`The StackBite backend record ${recordPath} is invalid for this state; WorkBite does not replace it. ${RECOVERY}`);
472
+ }
473
+ checkBoot(saved, recordPath, boot);
474
+ requireExecutionMarker(saved, execution, recordPath, 'StackBite backend', NATIVE_LAUNCH);
475
+ if (!await beforeDeadline(() => identityImpl(saved.url, layout.stackbite, backendId, { timeout: Math.min(5000, timeout) }),
476
+ performance.now() + timeout, `StackBite backend authentication timed out; outcome unknown; record retained: ${recordPath}; logs: ${layout.logs}`)) {
477
+ if (!await archivePreviousRecord(recordPath, saved, original, boot))
478
+ fail(`The StackBite backend recorded in ${recordPath} could not be authenticated (still starting, or stopped). ${RECOVERY}`);
479
+ } else {
480
+ if (saved.state === 'starting' || validBoot(boot))
481
+ await writeAtomic(recordPath, recordBytes({ ...saved, ...(validBoot(boot) ? { boot } : {}), state: 'ready' }));
482
+ return { url: saved.url, reused: true, pid: saved.pid };
483
+ }
484
+ }
485
+ if (!validBoot(boot)) fail('Trusted OS boot provenance is unavailable; no backend was started.');
486
+ const port = await freePort();
487
+ const url = `http://127.0.0.1:${port}`;
488
+ const intent = { boot, schema: STACKBITE_SCHEMA, state: 'starting', backend_id: backendId, launch: randomBytes(8).toString('hex'), url, port,
489
+ ...executionFields(execution, NATIVE_LAUNCH) };
490
+ const log = logFor(intent.launch);
491
+ const home = path.dirname(commands.stackbite[commands.stackbite.length - 1]);
492
+ await writeAtomic(recordPath, recordBytes(intent));
493
+ // The private Python terminal core and this backend's own loopback bridge are
494
+ // mandatory for the room backend (taskrooms reads STACKBITE_BRIDGE_URL; the agent
495
+ // CLI and frontend read STACKBITE_ROOM_BRIDGE_URL); caller values are overridden here only.
496
+ // A v2 bundle also starts it with the WorkBite native launch opt-in (cleanEnvironment drops any
497
+ // caller value, so a v1 backend never gets it); STACKBITE_HOME lets it resolve the bundle's native assets.
498
+ const { child, exited } = spawnDetached(commands.stackbite, ['--room-backend', '--port', String(port)], {
499
+ env: withPath({ ...environment, STACKBITE_HOME: home, STACKBITE_STATE_DIR: layout.stackbite,
500
+ STACKBITE_PTY_BACKEND: 'private', STACKBITE_ROOM_BACKEND: '1', STACKBITE_TERMINAL_CORE: 'python',
501
+ STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, PYINSTALLER_RESET_ENVIRONMENT: '1',
502
+ ...(execution ? { STACKBITE_WORKBITE_NATIVE_LAUNCH: '1' } : {}) }, home),
503
+ cwd: home, log, spawnImpl });
504
+ await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid }));
505
+ await waitFor(left => identityImpl(url, layout.stackbite, backendId, { timeout: Math.min(5000, left) }),
506
+ exited, timeout, 'StackBite backend', log);
507
+ await writeAtomic(recordPath, recordBytes({ ...intent, pid: child.pid, state: 'ready' }));
508
+ return { url, reused: false, pid: child.pid };
509
+ }
510
+
511
+ // v2 only, after this backend's identity is proven: one headless `stackbite --workbite-native-init`
512
+ // selects (or, through StackBite's own durable coordinator, reuses) the native terminal daemon for
513
+ // this backend. No UI, window, Room or Seat; exactly one exit-0 selected record is accepted.
514
+ // Anything else (refused, Unknown, malformed, timed out) refuses the open: WorkBite never retries,
515
+ // starts another daemon or falls back, and a later open asks the same coordinator again.
516
+ const NATIVE_INIT_SCHEMA = 'stackbite.workbite-native-init.v1';
517
+ async function initializeNativeRuntime({ commands, layout, url, environment, execImpl = execFile, timeout = 120000 }) {
518
+ const home = path.dirname(commands.stackbite[commands.stackbite.length - 1]);
519
+ const [file, ...prefix] = commands.stackbite;
520
+ const result = await runOnce(file, [...prefix, '--workbite-native-init'],
521
+ withPath({ ...environment, STACKBITE_HOME: home, STACKBITE_STATE_DIR: layout.stackbite,
522
+ STACKBITE_BRIDGE_URL: url, STACKBITE_ROOM_BRIDGE_URL: url, STACKBITE_WORKBITE_NATIVE_LAUNCH: '1',
523
+ PYINSTALLER_RESET_ENVIRONMENT: '1' }, home),
524
+ timeout, execImpl, { cwd: home, maxBuffer: 64 * 1024 });
525
+ const text = result.stdout.trim();
526
+ let record = null;
527
+ if (text && !/[\r\n]/.test(text) && text.length <= 4096) { try { record = JSON.parse(text); } catch { record = null; } }
528
+ if (result.error || record === null || typeof record !== 'object' || Array.isArray(record) || Object.keys(record).length !== 2
529
+ || record.schema !== NATIVE_INIT_SCHEMA || record.status !== 'selected') {
530
+ const exit = result.error ? (result.error.killed ? 'timed out' : `exit ${result.error.code ?? 'unknown'}`) : 'no selected record';
531
+ fail(`StackBite native runtime initialization did not select a native runtime (${exit}: ${boundedText(result.stderr) || 'no reason'}); `
532
+ + 'the execution UI was not started. WorkBite does not retry, start another daemon or fall back; '
533
+ + 'if the outcome is unknown, open again later and StackBite looks the same selection up.');
534
+ }
535
+ return record.status;
536
+ }
537
+
538
+ async function writeDockConfig(layout, url) {
539
+ const bytes = Buffer.from(JSON.stringify({ schema: CONNECTION_SCHEMA, base_url: url, state_dir: layout.stackbite }, null, 2) + '\n');
540
+ let current = null;
541
+ try { current = await readSmall(layout.dock, 65536); } catch { current = null; }
542
+ if (!current || !current.equals(bytes)) await writeAtomic(layout.dock, bytes);
543
+ }
544
+
545
+ // WB-EXEC-UI-V1 §4.4 device opt-in for 9 Execution, in WorkBite's own private settings file.
546
+ // Written only when no execution_ui choice exists; a present choice (enabled or disabled, any
547
+ // prefix) is kept byte for byte, and anything else there is refused, never repaired.
548
+ const EXECUTION_UI_SCHEMA = 'workbite.execution-ui-settings.v1';
549
+ const EXECUTION_UI_DEFAULT = Object.freeze({ schema: EXECUTION_UI_SCHEMA, enabled: true, prefix: 'ctrl+]', last_view_execution: false });
550
+ const SETTINGS_FIX = 'WorkBite does not repair or replace it; fix it or move it aside and open again.';
551
+ function validExecutionUi(value) {
552
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 4
553
+ && value.schema === EXECUTION_UI_SCHEMA && typeof value.enabled === 'boolean' && typeof value.prefix === 'string'
554
+ && typeof value.last_view_execution === 'boolean';
555
+ }
556
+ // Same-directory temporary file published by hard link: never replaces a file another writer created.
557
+ async function publishNew(file, bytes) {
558
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${randomBytes(8).toString('hex')}.tmp`);
559
+ const handle = await fs.open(temporary, 'wx', 0o600);
560
+ try { await handle.writeFile(bytes); await handle.sync(); } finally { await handle.close(); }
561
+ try { await fs.link(temporary, file); return true; } catch (error) {
562
+ if (error.code !== 'EEXIST') throw error;
563
+ return false;
564
+ } finally { await fs.unlink(temporary); }
565
+ }
566
+ async function ensureExecutionSettings(file, attempt = 0) {
567
+ let bytes = null;
568
+ try { bytes = await readSmall(file, 1024 * 1024); } catch (error) {
569
+ if (error.code !== 'ENOENT') fail(`The execution UI settings ${file} are unreadable. ${SETTINGS_FIX}`);
570
+ }
571
+ if (bytes === null) {
572
+ const created = await publishNew(file, Buffer.from(JSON.stringify({ execution_ui: EXECUTION_UI_DEFAULT }, null, 2) + '\n'));
573
+ if (created) return 'initialized';
574
+ if (attempt) fail(`The execution UI settings ${file} keep changing while opening. ${SETTINGS_FIX}`);
575
+ return ensureExecutionSettings(file, attempt + 1);
576
+ }
577
+ let document;
578
+ try {
579
+ const text = bytes.toString('utf8');
580
+ document = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text);
581
+ } catch { fail(`The execution UI settings ${file} are not valid JSON. ${SETTINGS_FIX}`); }
582
+ if (document === null || typeof document !== 'object' || Array.isArray(document)) {
583
+ fail(`The execution UI settings ${file} are not a JSON object. ${SETTINGS_FIX}`);
584
+ }
585
+ if (!Object.hasOwn(document, 'execution_ui')) {
586
+ // Other settings are kept; only the missing opt-in is added.
587
+ await writeAtomic(file, Buffer.from(JSON.stringify({ ...document, execution_ui: EXECUTION_UI_DEFAULT }, null, 2) + '\n'));
588
+ return 'initialized';
589
+ }
590
+ if (!validExecutionUi(document.execution_ui)) {
591
+ fail(`The execution UI settings ${file} hold an execution_ui value that is not the exact four-key opt-in object `
592
+ + `(schema "${EXECUTION_UI_SCHEMA}", enabled, prefix, last_view_execution). ${SETTINGS_FIX}`);
593
+ }
594
+ return document.execution_ui.enabled ? 'enabled' : 'disabled';
595
+ }
596
+
597
+ // The TypeScript UI's same-window 9 Execution (docs/WORKBITE-TYPESCRIPT-EXECUTION-CONFIG.md): set only by
598
+ // this launcher for a verified v2 bundle whose device opt-in is enabled. The bridge is the bundle's own
599
+ // verified workbite_tui (run by the UI with its fixed --workbite-execution-bridge argv), never a caller,
600
+ // receipt or PATH value; the UI keeps its own WORKSTACK_TUI_SETTINGS, so these names carry the private
601
+ // execution settings and intent paths separately. cleanEnvironment drops any caller WORKBITE_ value first.
602
+ function typescriptExecutionEnvironment(layout, bridge) {
603
+ return { WORKBITE_EXECUTION_UI: '1', WORKBITE_EXECUTION_BRIDGE: bridge,
604
+ WORKBITE_EXECUTION_SETTINGS: layout.tuiSettings, WORKBITE_EXECUTION_INTENT: layout.tuiIntent };
605
+ }
606
+ const verifiedPath = value => typeof value === 'string' && path.isAbsolute(value) && !/[\x00-\x1f]/.test(value);
607
+
608
+ function uiEnvironment(base, { executables, layout, endpoint, uid, backendId, execution = null, bridge = null, mobile = null }) {
609
+ const home = path.dirname(executables.stackbite);
610
+ return withPath({ ...base, WORKSTACK_TRANSPORT: 'native', WORKSTACK_NATIVE_CLIENT: executables.workstack_client,
611
+ WORKSTACK_ENDPOINT: endpoint, WORKSTACK_WORKSPACE_UID: uid, WORKSTACK_STATE_DIR: layout.tuiState,
612
+ WORKSTACK_EXECUTION_DOCK_CONFIG: layout.dock, WORKSTACK_EXECUTION_DOCK_BACKEND_ID: backendId,
613
+ WORKSTACK_DEPLOY_JOURNAL_DIR: layout.journal, WORKSTACK_STACKBITE_EXECUTABLE: executables.stackbite,
614
+ ...(execution ? { WORKSTACK_TUI_STATE: layout.tuiIntent, WORKSTACK_TUI_SETTINGS: layout.tuiSettings } : {}),
615
+ ...(bridge ? typescriptExecutionEnvironment(layout, bridge) : {}),
616
+ ...(mobile ? { WORKBITE_MOBILE_EXECUTABLE: executables.workbite_mobile,
617
+ WORKBITE_MOBILE_HOST: JSON.stringify(mobile) } : {}) }, home);
618
+ }
619
+
620
+ function runForeground(command, args, environment, { spawnImpl = spawn, signalSource = process,
621
+ timeout = 30000, onStarted = () => {} } = {}) {
622
+ return new Promise((resolve, reject) => {
623
+ const [file, ...prefix] = command;
624
+ const child = spawnImpl(file, [...prefix, ...args], { env: environment, stdio: 'inherit', shell: false });
625
+ let settled = false;
626
+ const handlers = new Map();
627
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
628
+ const handler = () => child.kill(signal);
629
+ handlers.set(signal, handler);
630
+ signalSource.on(signal, handler);
631
+ }
632
+ const timer = setTimeout(() => {
633
+ settled = true;
634
+ cleanup();
635
+ reject(new Error('Work Stack terminal UI launch timed out; outcome unknown. No retry or fallback was attempted.'));
636
+ }, finiteTimeout(timeout, 30000));
637
+ const cleanup = () => {
638
+ clearTimeout(timer);
639
+ for (const [signal, handler] of handlers) signalSource.removeListener(signal, handler);
640
+ };
641
+ // Only launch acknowledgement is bounded. A successfully spawned UI runs until it exits.
642
+ child.once('spawn', () => { if (!settled) { clearTimeout(timer); onStarted(); } });
643
+ child.once('error', () => { settled = true; cleanup(); reject(new Error('Cannot start the verified Work Stack terminal UI')); });
644
+ child.once('exit', (code, signal) => { settled = true; cleanup(); resolve({ code, signal }); });
645
+ });
646
+ }
647
+
648
+ // commands maps each bundle executable to its argv prefix; tests substitute fixtures.
649
+ async function openSuite({ installed, commands, workspace, init = false, stateRoot, environment = process.env,
650
+ ui = true, frontend = 'typescript', spawnImpl = spawn, execImpl = execFile, platform = process.platform,
651
+ stdout = process.stdout, stderr = process.stderr, timeouts = {}, bootImpl = bootIdentity, mobileBootstrap = undefined }) {
652
+ if (!FRONTENDS.includes(frontend)) fail(`--frontend must be one of: ${FRONTENDS.join(', ')}`);
653
+ if (workspace !== undefined) workspace = checkedWorkspace(workspace);
654
+ else if (init) fail('--init needs --workspace ABS naming the new or empty folder');
655
+ commands = commands || Object.fromEntries(Object.entries(installed.executables).map(([name, file]) => [name, [file]]));
656
+ // A v2 bundle keeps its owner/backend markers and native init whichever frontend runs; only the
657
+ // foreground UI differs. TypeScript is the default; rust needs a v2 bundle. There is no fallback between them.
658
+ const execution = installed.bundle?.schema === BUNDLE_V2 ? { bundleVersion: installed.bundle.version } : null;
659
+ if (frontend === 'rust' && !execution) {
660
+ fail('--frontend rust needs a WorkBite execution (v2) bundle; this installed bundle has only the TypeScript UI. Nothing was started.');
661
+ }
662
+ const uiCommand = frontend === 'rust' ? 'workbite_tui' : 'workstack_tui';
663
+ if (!(Array.isArray(commands[uiCommand]) && commands[uiCommand].length)) {
664
+ fail(frontend === 'rust' ? 'This WorkBite bundle has no verified execution UI executable; nothing was started.'
665
+ : 'This WorkBite bundle has no verified TypeScript UI executable; nothing was started.');
666
+ }
667
+ // A verified v2 bundle always holds the execution bridge; anything else is refused, never probed for.
668
+ const bridge = execution && frontend === 'typescript' ? installed.executables?.workbite_tui : null;
669
+ if (execution && frontend === 'typescript' && !verifiedPath(bridge)) {
670
+ fail('This WorkBite execution bundle has no verified execution bridge executable; nothing was started.');
671
+ }
672
+ const layout = stateLayout(path.resolve(stateRoot));
673
+ const stage = async (name, action) => {
674
+ stderr.write(`[workbite] ${name}: starting\n`);
675
+ try {
676
+ const value = await action();
677
+ stderr.write(`[workbite] ${name}: ready\n`);
678
+ return value;
679
+ } catch (error) {
680
+ if (error.workbiteStage) throw error;
681
+ stderr.write(`[workbite] ${name}: failed\n`);
682
+ const wrapped = new Error(`WorkBite startup stage "${name}" failed: ${error.message}; state: ${layout.root}; logs: ${layout.logs}`,
683
+ { cause: error });
684
+ wrapped.workbiteStage = name;
685
+ throw wrapped;
686
+ }
687
+ };
688
+ // Fixed stage names only: never print child environment, tokens or native stdout.
689
+ await stage('state preparation', () => prepareState(layout));
690
+ const base = cleanEnvironment(environment);
691
+ const startupExec = (file, args, options, callback) => {
692
+ runOnce(file, args, options.env, options.timeout, execImpl, options)
693
+ .then(result => callback(result.error, result.stdout,
694
+ result.stderr || (result.error?.killed ? 'startup command timed out; outcome unknown' : '')));
695
+ };
696
+ if (execution) {
697
+ // The execution UI's intent parent and deploy journal, made private (Windows DACL) before any file in them is written.
698
+ await stage('private execution state', async () => {
699
+ await safeDirectory(layout.nativeTui);
700
+ await protectPrivateDirectories([layout.nativeTui, layout.journal], { platform, environment: base,
701
+ execImpl: startupExec, timeout: finiteTimeout(timeouts.privateState, 60000) });
702
+ });
703
+ }
704
+ const engineEnvironment = { ...base, WORK_STACK_RUNTIME: layout.workstackRuntime };
705
+ const session = await stage('startup lock', () => withLock(layout.lock, async () => {
706
+ let remembered = null;
707
+ if (workspace === undefined) {
708
+ remembered = await stage('workspace selection', () => rememberedWorkspace(layout));
709
+ workspace = checkedWorkspace(remembered.workspace);
710
+ }
711
+ // Settings are settled before any workspace init or backend start, so a refused file changes nothing else.
712
+ const executionUi = execution ? await stage('execution settings', () => ensureExecutionSettings(layout.tuiSettings)) : null;
713
+ const uid = await stage(init ? 'workspace initialization' : 'workspace identity', () => init
714
+ ? initializeWorkspace(commands, workspace, engineEnvironment, { timeout: timeouts.workspaceInit }) : readWorkspaceUid(workspace));
715
+ if (remembered && uid !== remembered.uid) fail(`The workspace ${workspace} changed while opening. ${EXPLICIT}`);
716
+ const backendId = await stage('backend identity', () => ensureBackendId(layout));
717
+ const boot = await stage('host boot provenance', () => beforeDeadline(() => bootImpl({ platform, environment: base }),
718
+ performance.now() + finiteTimeout(timeouts.boot, 10000), 'OS boot provenance timed out; no backend was started'));
719
+ const engine = await stage('Work Stack owner', () => ensureEngine({ commands, workspace, uid, layout, environment: engineEnvironment, spawnImpl,
720
+ timeout: timeouts.engine, execution, boot }));
721
+ const stackbite = await stage('StackBite backend', () => ensureStackbite({ commands, layout, backendId, environment: base, spawnImpl,
722
+ timeout: timeouts.stackbite, execution, boot }));
723
+ if (execution) {
724
+ await stage('native runtime selection', () => initializeNativeRuntime({ commands, layout, url: stackbite.url, environment: base, execImpl,
725
+ timeout: timeouts.nativeInit }));
726
+ }
727
+ await stage('execution connection', () => writeDockConfig(layout, stackbite.url));
728
+ // Only after the owner, StackBite and dock config are proven ready, and before the UI.
729
+ await stage('workspace selection save', () => rememberWorkspace(layout, workspace, uid));
730
+ return { uid, backendId, engine, stackbite, executionUi };
731
+ }));
732
+ const summary = { schema: 'workbite.open.v1', workspace, workspace_uid: session.uid, endpoint: session.engine.endpoint,
733
+ owner_reused: session.engine.reused, stackbite_url: session.stackbite.url, stackbite_reused: session.stackbite.reused,
734
+ backend_id: session.backendId, state: layout.root, frontend,
735
+ ...(execution ? { execution_ui: session.executionUi, execution_bundle_version: execution.bundleVersion } : {}) };
736
+ if (!ui) {
737
+ stdout.write(JSON.stringify(summary) + '\n');
738
+ return { code: 0, signal: null, summary };
739
+ }
740
+ const environmentForUi = uiEnvironment(base, { executables: installed.executables, layout, endpoint: session.engine.endpoint,
741
+ uid: session.uid, backendId: session.backendId, execution: frontend === 'rust' ? execution : null,
742
+ bridge: bridge && ['enabled', 'initialized'].includes(session.executionUi) ? bridge : null });
743
+ if (frontend === 'typescript' && platform === 'win32' && verifiedPath(installed.executables?.workbite_mobile)) {
744
+ const mobileRoot = path.join(layout.root, 'mobile');
745
+ await stage('private phone state', async () => {
746
+ await safeDirectory(mobileRoot);
747
+ await protectPrivateDirectories([mobileRoot], { platform, environment: base, execImpl: startupExec,
748
+ timeout: finiteTimeout(timeouts.privateState, 60000) });
749
+ });
750
+ environmentForUi.WORKBITE_MOBILE_EXECUTABLE = installed.executables.workbite_mobile;
751
+ environmentForUi.WORKBITE_MOBILE_HOST = JSON.stringify({
752
+ directory: path.join(mobileRoot, workspaceKey(workspace)), executable: installed.executables.workbite_mobile,
753
+ scope: { host_id: session.backendId, workspace_uid: session.uid },
754
+ native: { executable: installed.executables.workstack_client, cwd: workspace, endpoint: session.engine.endpoint,
755
+ workspaceUid: session.uid, deployment: { connection: layout.dock, backendId: session.backendId, journalDir: layout.journal } },
756
+ // Embedding hosts may supply the private operator invitation at first setup; never a token.
757
+ ...(mobileBootstrap ? { initialInput: mobileBootstrap } : {}),
758
+ });
759
+ }
760
+ // Exactly one foreground child in this terminal (inherited stdio, never detached or a new console).
761
+ // The TypeScript UI gets the v1 environment (keeping its own familiar settings file) plus, when the
762
+ // v2 opt-in is enabled, only the WORKBITE_EXECUTION_* names above.
763
+ stderr.write('[workbite] terminal UI launch: starting\n');
764
+ const result = await runForeground(commands[uiCommand], [], environmentForUi, { spawnImpl, timeout: timeouts.uiLaunch,
765
+ onStarted: () => stderr.write('[workbite] terminal UI launch: ready\n') }).catch(error => {
766
+ stderr.write('[workbite] terminal UI launch: failed\n');
767
+ throw new Error(`WorkBite startup stage "terminal UI launch" failed: ${error.message}; state: ${layout.root}; logs: ${layout.logs}`, { cause: error });
768
+ });
769
+ return { ...result, summary };
770
+ }
771
+
772
+ module.exports = { cleanEnvironment, stateLayout, prepareState, ensureBackendId, workspaceKey, readWorkspaceUid,
773
+ initializeWorkspace, engineIdentity, stackbiteIdentity, freePort, ensureEngine, ensureStackbite, writeDockConfig,
774
+ withLock, uiEnvironment, openSuite, rememberedWorkspace, ensureExecutionSettings, initializeNativeRuntime, RESERVED_PORTS,
775
+ SELECTION_SCHEMA, EXECUTION_UI_DEFAULT, NATIVE_INIT_SCHEMA, FRONTENDS, runForeground };