mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-28 15:41:57 +02:00
* 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
232 lines
16 KiB
JavaScript
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;
|
|
});
|
|
}
|