Files
gstack/.github/scripts/dia-launch-driver.mjs
T
Garry Tan a84b0b5b6d v1.90.0.0 feat: make browser cookie imports explicit and safe (#2964)
* fix(browse): prepare reliable cookie import wave for validation

* ci: sequence quality and behavior for validation branch

* fix(browse): isolate Windows qualification and preserve native diagnostics

* test(browse): cover cookie workflow quality and isolate Windows user paths

* test(browse): trace native member startup and initialize fresh folders

* fix(browse): keep Windows member stdin alive through EOF

* fix(browse): latch native timeouts and compare contained Edge startup

* test(browse): verify native version metadata and actual Windows argv

* test(browse): qualify Dia import on isolated macOS CI

* fix(browse): require picker origin for session mutations

* fix(browse): bound credential reads through stream completion

* test(browse): inspect owned Windows process arguments natively

* test(evals): preserve passing coverage during cookie repair reruns

* test(browse): isolate Dia qualification in a fresh macOS account

* test(browse): pass bounded integer timeouts to native Mac probes

* test(browse): distinguish Windows profile initialization from containment

* test(browse): await descendant pipe readiness before parent exit

* test(browse): initialize and restore isolated macOS Keychain state

* test(browse): initialize Windows fixture folders before qualification

* test(ci): pin the same Node runtime across Windows checks

* test(browse): distinguish native macOS browser preflight stages

* test(browse): isolate Windows descendant console lifetime

* test(browse): preserve native receipts and identify fixture lock holders

* test(browse): prepare dependency resolution before native Mac worker startup

* test(ci): include lock and close checks in native diagnostics

* test(browse): preserve native owner probe stages and subprocess deadlines

* fix(browse): classify Chromium profile-in-use exit precisely

* test(browse): retain Mac qualification evidence through cleanup failures

* test(browse): bound Mac fixture paths and retire its owned user domain

* test(browse): accept vanished fixture entries without weakening cleanup

* test(browse): identify probe-created macOS user domains safely

* test(browse): observe Mac user domains without targeting them first

* test(browse): use passive fresh-user ownership throughout Mac qualification

* test(browse): distinguish profile and registered-home Keychain lookups

* test(browse): qualify Dia under one registered account home

* test(browse): identify Dia startup and owned process-group failures

* test(browse): classify bounded Dia startup diagnostics without leaking output

* fix(test): preserve native Mac sandboxing and reap owned browser children

* fix(browse): preserve Chromium sandboxing for native profile imports

* test(browse): inspect signed Mach-O architecture without launching Xcode tools

* test(browse): sample pending Dia startup and reap on all cleanup paths

* test(browse): compare protected Dia launches in fresh Bun and Node accounts

* test(browse): inspect isolated Mac GUI readiness without browser access

* v1.90.0.0 fix: bind cookie picker actions to their document

* test: validate cookie guards and fit nested launch fixtures

* ci: configure the bundled Chromium sandbox helper

* fix(browse): classify Playwright authentication timeouts

* test: retain bounded Windows lifecycle diagnostics

* test(cso): reuse bounded NTFS precision candidates

* test(review): handle explicit preservation choices safely

* test(browse): remove owned fixture directories with explicit primitives

* test(review): distinguish descriptive reuse from edit commitments

* test: admit only the approved unscored cookie workflow refusal

* test: keep the Office Hours judge mock export-complete

* fix: keep dependency-free CI planners independent of the model SDK

* test: observe the exact holder after a native fixture unlink failure

* fix: start seeded PTY observations at owned readiness

* test: acquire identity-bound Windows deletion admission before profile resets

* test: preserve qualified Git index bits without authorizing mutations
2026-09-25 12:06:45 -04:00

232 lines
16 KiB
JavaScript

import { createHash } from 'node:crypto';
import { closeSync, constants, createReadStream, fstatSync, lstatSync, openSync, readdirSync, readSync, realpathSync } from 'node:fs';
import { createRequire } from 'node:module';
import { release } from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { assertOwnedDiaProfile, browserOperationTimedOut, browserPreflightError, browserRootFacts, browserStartupFacts, browserStderrFacts,
nativeDiaLaunchOptions, observeBrowserLaunches, ownsFreshAccount, parseDirectoryRecord,
readFreshAccountConfiguration, stopOwnedBrowserGroup, validateQualificationHost } from './qualify-dia-macos.ts';
const require = createRequire(import.meta.url);
const sha256 = async file => {
const hash = createHash('sha256');
for await (const chunk of createReadStream(file)) hash.update(chunk);
return hash.digest('hex');
};
export function normalizedLaunchHashes(args, env, ownedPaths) {
const paths = Object.entries(ownedPaths).sort(([, left], [, right]) => right.length - left.length);
const normalize = value => {
const equals = value.startsWith('--') ? value.indexOf('=') : -1;
const prefix = equals >= 0 ? value.slice(0, equals + 1) : '';
const candidate = equals >= 0 ? value.slice(equals + 1) : value;
for (const [role, owned] of paths) {
if (candidate === owned || candidate.startsWith(owned + path.sep)) return prefix + '<' + role + '>' + candidate.slice(owned.length);
}
return value;
};
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
return { argvSha256: hash(args.map(normalize)), environmentSha256: hash(Object.entries(env).sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => [key, normalize(value)])) };
}
export function compareDiaLaunchReceipts(left, right) {
const invalid = { comparable: false, qualificationCredit: false, reason: 'incomplete_or_incompatible_arms' };
if (!left?.launcher?.accountGuid || left.launcher.accountGuid === right?.launcher?.accountGuid) return invalid;
const fingerprints = [];
for (const [receipt, runtime] of [[left, 'bun'], [right, 'node']]) {
const qualification = receipt?.qualification;
const control = receipt?.backgroundPreflight?.comparisonControl;
const source = qualification?.launchComparison?.source;
const config = receipt?.launchComparison;
if (config?.mode !== 'launch-only' || config.runtime !== runtime || config.qualificationCredit !== false
|| receipt.backgroundPreflight?.status !== 'passed' || !control?.ready || qualification?.launchComparison?.qualificationCredit !== false
|| qualification.keychainStage !== 'completed' || qualification.isolation?.registeredIdentity !== true
|| qualification.isolation?.sharedRegisteredHome !== true || qualification.artifact?.signatureVerified !== true
|| qualification.artifact?.gatekeeperNotarized !== true || qualification.artifact?.macosCompatibility?.compatible !== true
|| qualification.platform?.os !== 'darwin' || qualification.platform?.architecture !== 'arm64'
|| qualification.platform?.bun !== '1.4.0' || qualification.platform?.playwright !== '1.62.1'
|| !/^\d+(?:\.\d+){1,2}$/.test(qualification.artifact.macosCompatibility.hostVersion)
|| qualification.artifact?.architectures?.includes('arm64') !== true
|| ['serviceStopped', 'userDomainStopped', 'userProcessesStopped', 'accountRemoved', 'groupRemoved', 'stagingRemoved'].some(key => receipt.launcherCleanup?.[key] !== true)
|| ['ownedBrowsersStopped', 'sourceProfileRemoved', 'keychainRestored', 'mountDetached', 'fixtureRemoved'].some(key => qualification.cleanup?.[key] !== true)
|| ['pass', 'fail', 'skip'].some(key => receipt.counts?.[key] !== 0 || qualification.counts?.[key] !== 0)) return invalid;
for (const [result, purpose] of [[control, 'control'], [source, 'source']]) {
const policy = result?.launchAttempts?.[0];
if (result?.protocol !== 1 || result.purpose !== purpose || result.samplingEnabled !== false || result.rootCount !== 1
|| result.supervisor?.closed !== true || result.supervisor?.exitCode !== 0 || result.cleanup?.confirmed !== true
|| result.cleanup?.childClosed !== true || result.cleanup?.groupAbsent !== true || result.cleanup?.launchSettled !== true || result.launchAttempts?.length !== 1
|| result.cleanup?.groups?.length !== 1 || result.cleanup.groups[0].absenceConfirmed !== true || result.cleanup.groups[0].childCloseObserved !== true
|| typeof result.ready !== 'boolean' || typeof result.launchReturned !== 'boolean'
|| policy?.sandboxRequired !== true || policy?.sandboxDisablingFlag !== false || policy?.pipeFlag !== true || policy?.tcpDebuggingFlag !== false
|| policy?.mockKeychainFlag !== false || policy?.passwordStoreFlag !== false || policy?.firstRunSuppressed !== false
|| policy?.headlessFlag !== true || policy?.expectedProfile !== true || policy?.detached !== true || policy?.shellDisabled !== true
|| policy?.stdioCount !== 5 || policy?.extraPipeDescriptors !== true || policy?.profileArgumentCount !== 1
|| result.driver?.runtime !== runtime || result.driver?.version !== (runtime === 'bun' ? '1.4.0' : '24.18.0')
|| result.driver?.os !== 'darwin' || result.driver?.architecture !== 'arm64' || result.driver?.playwright !== '1.62.1'
|| result.driver?.release !== qualification.platform.release
|| result.driver?.executableSha256 !== config.executableSha256 || result.driver?.driverSha256 !== config.driverSha256
|| result.driver?.helpersSha256 !== config.helpersSha256 || (result.ready && (!result.protocolResponded || !result.startupPages?.allowed || !result.postProbePages?.allowed))
|| ![result.argvSha256, result.environmentSha256, config.executableSha256, config.driverSha256, config.helpersSha256].every(value => /^[a-f0-9]{64}$/.test(value))) return invalid;
}
const hashes = [receipt.launcher?.sourceRevision, receipt.launcher?.archiveSha256, receipt.launcher?.destinationSha256,
qualification.artifact.sha256, qualification.artifact.executableSha256];
if (!/^[a-f0-9]{40}$/.test(hashes[0]) || !hashes.slice(1).every(value => /^[a-f0-9]{64}$/.test(value))) return invalid;
fingerprints.push(JSON.stringify({ hashes, platform: qualification.platform, compatibility: qualification.artifact.macosCompatibility,
version: qualification.artifact.version, bundle: qualification.artifact.bundleId, team: qualification.artifact.team,
driver: config.driverSha256, helpers: config.helpersSha256, controlArgs: control.argvSha256, controlEnv: control.environmentSha256,
sourceArgs: source.argvSha256, sourceEnv: source.environmentSha256 }));
}
if (fingerprints[0] !== fingerprints[1]) return { ...invalid, reason: 'comparison_inputs_differ' };
const bun = left.qualification.launchComparison.source.ready === true;
const node = right.qualification.launchComparison.source.ready === true;
return { comparable: true, qualificationCredit: false, outcome: bun ? node ? 'both_ready' : 'bun_only_ready' : node ? 'node_only_ready' : 'neither_ready' };
}
export async function runProtectedLaunch(executable, profile, env, purpose, ownedPaths) {
const result = { protocol: 1, purpose, stage: 'runtime_import', launchReturned: false, protocolResponded: false, ready: false,
timedOut: false, error: null, samplingEnabled: false, cleanup: { childClosed: false, groupAbsent: false, confirmed: false } };
const { chromium } = await import('playwright');
const cp = require('node:child_process');
const original = cp.spawn;
cp.spawn = function(command, args, options) {
if (command === executable) Object.assign(result, normalizedLaunchHashes(args, options.env, ownedPaths));
return original.call(this, command, args, options);
};
const observer = observeBrowserLaunches(new Map([[executable, profile]]));
let context;
let timer;
let launchSettled = false;
try {
result.stage = 'launch';
const launch = chromium.launchPersistentContext(profile, nativeDiaLaunchOptions(executable, env));
void launch.then(() => { launchSettled = true; }, () => { launchSettled = true; });
context = await Promise.race([launch,
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('operation_timeout')), 30_000); })]);
clearTimeout(timer);
result.launchReturned = true;
result.stage = 'ownership';
if (observer.children.length !== 1) throw new Error('source_process_ownership_unconfirmed');
result.stage = 'startup_pages';
const urls = context.pages().map(page => page.url());
result.startupPages = { ...browserStartupFacts(urls, 'http://127.0.0.1:1'), allowed: urls.every(url => url === 'about:blank') };
if (!result.startupPages.allowed) throw new Error('onboarding_or_external_page');
result.stage = 'protocol_probe';
if (!context.pages()[0]) throw new Error('source_protocol_page_unavailable');
result.protocolResponded = await Promise.race([context.pages()[0].evaluate(() => 1).then(value => value === 1),
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('operation_timeout')), 5000); })]);
clearTimeout(timer);
const after = context.pages().map(page => page.url());
result.postProbePages = { ...browserStartupFacts(after, 'http://127.0.0.1:1'), allowed: after.every(url => url === 'about:blank') };
if (!result.postProbePages.allowed) throw new Error('onboarding_or_external_page');
result.ready = result.protocolResponded;
result.stage = 'ready';
} catch (error) {
const reasons = browserStderrFacts(observer.children).flatMap(facts => Object.entries(facts.reasonCounts ?? {})
.filter(([, count]) => count > 0).map(([reason]) => reason));
result.error = browserPreflightError(error, reasons);
result.timedOut = browserOperationTimedOut(error);
} finally {
clearTimeout(timer);
const deadline = performance.now() + 5_000;
observer.stop();
result.rootCount = observer.children.length;
result.launchAttempts = observer.attempts;
result.rootsBeforeCleanup = browserRootFacts(observer.children);
result.stderrBeforeCleanup = browserStderrFacts(observer.children);
if (context) void context.close().catch(() => {});
const groups = [];
for (const child of observer.children) {
const facts = { pid: child.pid, signalSent: false, absenceConfirmed: false, childCloseObserved: false };
try { await stopOwnedBrowserGroup(child, deadline, facts); }
catch { facts.failed = true; }
groups.push(facts);
}
result.cleanup.groups = groups;
result.cleanup.childClosed = observer.children.length === 1 && observer.children.every(child => child.closeObserved);
result.cleanup.groupAbsent = groups.length === 1 && groups.every(group => group.absenceConfirmed);
result.cleanup.launchSettled = launchSettled;
result.cleanup.confirmed = result.cleanup.childClosed && result.cleanup.groupAbsent && launchSettled;
result.rootsAfterCleanup = browserRootFacts(observer.children);
result.stderrAfterCleanup = browserStderrFacts(observer.children);
if (launchSettled) { observer.restore(); cp.spawn = original; }
}
return result;
}
export function readComparisonRequest() {
const buffer = Buffer.alloc(16 * 1024 + 1);
let length = 0;
while (length < buffer.length) {
const read = readSync(0, buffer, length, buffer.length - length, null);
if (!read) break;
length += read;
}
if (length > 16 * 1024) throw new Error('invalid_driver_request');
const request = JSON.parse(buffer.subarray(0, length).toString());
if (!request || !['control', 'source'].includes(request.purpose)
|| Object.keys(request).some(key => !(request.purpose === 'control' ? ['purpose'] : ['purpose', 'assetRoot', 'executableName', 'executableSha256']).includes(key))) throw new Error('invalid_driver_request');
return request;
}
async function main() {
validateQualificationHost(process.env);
const request = readComparisonRequest();
const account = readFreshAccountConfiguration(process.argv[2], 'comparison-driver');
const config = account.launchComparison;
const runtimeVersion = process.versions.bun ?? process.versions.node;
if ((config.runtime === 'bun' ? process.versions.bun !== '1.4.0' : Boolean(process.versions.bun) || runtimeVersion !== '24.18.0')
|| process.arch !== 'arm64' || require('playwright/package.json').version !== '1.62.1') throw new Error('invalid_driver_runtime');
if (await sha256(process.execPath) !== config.executableSha256 || await sha256(import.meta.filename) !== config.driverSha256
|| await sha256(path.join(import.meta.dirname, 'qualify-dia-macos.ts')) !== config.helpersSha256) throw new Error('driver_inputs_changed');
const identity = spawnSync('/usr/bin/dscl', ['.', '-read', '/Users/' + account.account, 'UniqueID', 'PrimaryGroupID', 'NFSHomeDirectory', 'GeneratedUID'],
{ env: account.environment, encoding: 'utf8', timeout: 5000, maxBuffer: 64 * 1024 });
if (identity.error || identity.status !== 0 || !ownsFreshAccount(parseDirectoryRecord(identity.stdout), account)) throw new Error('driver_identity_unconfirmed');
let executable = account.destinationExecutable;
let profile = path.join(account.temporary, 'probe/chromium');
let temporary = account.temporary;
const ownedPaths = { home: account.home, snapshot: account.snapshot, temporary: account.temporary, work: account.work };
if (request.purpose === 'source') {
const root = request.assetRoot;
if (typeof root !== 'string' || realpathSync(root) !== root || path.dirname(root) !== account.temporary || !path.basename(root).startsWith('dia-')
|| lstatSync(root).uid !== account.uid || typeof request.executableName !== 'string' || !request.executableName
|| path.basename(request.executableName) !== request.executableName || ['.', '..'].includes(request.executableName)) throw new Error('invalid_source_request');
executable = realpathSync(path.join(root, 'Dia.app/Contents/MacOS', request.executableName));
if (!executable.startsWith(path.join(root, 'Dia.app/Contents/MacOS') + path.sep)
|| !/^[a-f0-9]{64}$/.test(request.executableSha256) || await sha256(executable) !== request.executableSha256) throw new Error('source_identity_unconfirmed');
profile = path.join(account.home, 'Library/Application Support/Dia/User Data');
if (realpathSync(profile) !== profile || lstatSync(profile).uid !== account.uid) throw new Error('source_profile_unowned');
if (JSON.stringify(readdirSync(profile)) !== JSON.stringify(['.gstack-dia-owner'])) throw new Error('source_profile_not_fresh');
const descriptor = openSync(path.join(profile, '.gstack-dia-owner'), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
const marker = Buffer.alloc(65);
let bytes;
try {
const entry = fstatSync(descriptor);
if (!entry.isFile() || entry.uid !== account.uid || entry.nlink !== 1 || entry.size !== 64 || (entry.mode & 0o077) !== 0) throw new Error('source_profile_unowned');
bytes = readSync(descriptor, marker, 0, marker.length, 0);
} finally { closeSync(descriptor); }
if (bytes !== 64) throw new Error('source_profile_unowned');
const info = lstatSync(profile, { bigint: true });
assertOwnedDiaProfile({ home: account.home, profile, uid: account.uid, dev: info.dev, ino: info.ino, nonce: marker.subarray(0, bytes).toString() });
temporary = path.join(root, 't');
ownedPaths.assets = root;
} else {
if (await sha256(executable) !== account.destinationSha256) throw new Error('control_identity_unconfirmed');
try { lstatSync(profile); throw new Error('control_profile_not_fresh'); }
catch (error) { if (error.code !== 'ENOENT') throw error; }
}
const env = { HOME: account.home, TMPDIR: temporary, PATH: '/usr/bin:/bin:/usr/sbin:/sbin', LANG: 'en_US.UTF-8' };
const result = await runProtectedLaunch(executable, profile, env, request.purpose, ownedPaths);
result.driver = { runtime: config.runtime, version: runtimeVersion, architecture: process.arch, os: process.platform, release: release(),
executableSha256: config.executableSha256, driverSha256: config.driverSha256, helpersSha256: config.helpersSha256, playwright: '1.62.1' };
return result;
}
if (import.meta.main) {
main().then(result => { process.stdout.write(JSON.stringify(result) + '\n'); }, () => {
process.stdout.write(JSON.stringify({ protocol: 1, ready: false, error: 'driver_admission_failed', cleanup: { confirmed: false } }) + '\n');
process.exitCode = 2;
});
}