Files
GenericAgent/frontends/desktop/scripts/verify-compiled-dist.mjs
T
YI QI LIMandabraxas914 efb3bc6ad1 feat(desktop): compiled React Desktop 2.0 v0.2.1 renderer update (#787)
* fix(desktop): expose explicit backup capabilities

* fix(desktop): align titlebar controls with native metrics

* release(desktop): prepare portable v0.2.1

* build(desktop): rebuild compiled dist from source 4a67226

Rebuild the compiled-only React Desktop 2.0 renderer from abraxas914/GenericAgent
main @4a67226, carrying the v0.2.1 iterations that PR #784 was closed to await:
live turn-timer persistence across session switch, Mixin image-to-path fallback,
folder drag-drop, per-turn duration, and image-thumbnail repair.

- dist/**: regenerated 98 manifest assets + entry HTML
- build-provenance.json: sourceCommit -> 4a67226, manifest sha256 recomputed
- verify-compiled-dist.mjs: expectedSourceCommit -> 4a67226
- dist/README.md: provenance source commit updated
- desktop_bridge.py, test_bridge_origin_security.py: synced to 4a67226
- core (agentmain.py, llmcore*, src/**, public/**) remains zero-diff vs upstream

---------

Co-authored-by: abraxas914 <abraxas345589144233@outlook.com>
2026-08-30 14:21:51 +08:00

402 lines
17 KiB
JavaScript

import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { access, readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const desktopDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const repoRoot = path.resolve(desktopDir, '..', '..');
const distDir = path.join(desktopDir, 'dist');
const expectedVersion = '0.2.1';
const expectedSourceCommit = '4a67226e18cb003db1210357ea4a283774773acc';
const expectedNoticeSha256 = '2acf865e87e59090121369aac0575467067fdd7999923a70d785a46ceae3330f';
const noticeAttributeRules = [
'frontends/desktop/public/THIRD_PARTY_NOTICES.txt text eol=lf',
'frontends/desktop/dist/THIRD_PARTY_NOTICES.txt text eol=lf',
];
const excludedManifestFiles = new Set(['README.md', 'build-provenance.json']);
const removedLegacyAssets = [
'styles.css',
'i18n.js',
'phosphor-icons.js',
'vendor/marked.min.js',
'assets/fonts/fonts.css',
'assets/fonts/README.md',
'assets/fonts/azonix-wordmark.woff2',
'assets/fonts/jetbrains-mono-latin.woff2',
'assets/fonts/lexend-latin.woff2',
'assets/fonts/noto-sans-latin.woff2',
];
const semiPackages = [
'@douyinfe/semi-animation',
'@douyinfe/semi-animation-react',
'@douyinfe/semi-animation-styled',
'@douyinfe/semi-foundation',
'@douyinfe/semi-icons',
'@douyinfe/semi-illustrations',
'@douyinfe/semi-json-viewer-core',
'@douyinfe/semi-theme-default',
'@douyinfe/semi-ui',
];
function fail(message) {
throw new Error(message);
}
async function exists(file) {
try {
await access(file);
return true;
} catch {
return false;
}
}
async function readJson(file) {
return JSON.parse(await readFile(file, 'utf8'));
}
async function walk(dir) {
const entries = await readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) files.push(...await walk(fullPath));
if (entry.isFile()) files.push(fullPath);
}
return files;
}
function sha256(contents) {
return createHash('sha256').update(contents).digest('hex');
}
function relativeToDist(file) {
return path.relative(distDir, file).split(path.sep).join('/');
}
async function assertExists(relativePath) {
if (!await exists(path.join(distDir, relativePath))) {
fail(`missing compiled asset: ${relativePath}`);
}
}
function workflowJob(workflow, name) {
const match = new RegExp(`^ ${name}:\\s*$`, 'm').exec(workflow);
if (!match) return '';
const remainder = workflow.slice(match.index + match[0].length);
const nextJob = /^ [a-zA-Z0-9_-]+:\s*$/m.exec(remainder);
return nextJob ? remainder.slice(0, nextJob.index) : remainder;
}
async function verifyHtmlReferences(relativePath) {
const html = await readFile(path.join(distDir, relativePath), 'utf8');
const references = [...html.matchAll(/\b(?:src|href)=["']([^"']+)["']/g)].map((match) => match[1]);
for (const reference of references) {
if (/^(?:[a-z]+:|#|\/\/)/i.test(reference)) continue;
const cleanReference = reference.split(/[?#]/, 1)[0].replace(/^\//, '');
if (cleanReference) await assertExists(cleanReference);
}
}
function verifyTrackedSourceBoundary() {
const tracked = execFileSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8' })
.split('\n')
.filter(Boolean);
const forbidden = tracked.filter((file) => (
file.startsWith('frontends/desktop/src/')
|| file.startsWith('frontends/desktop/public/')
|| file.startsWith('frontends/desktop/e2e/')
|| /^frontends\/desktop\/(?:index|loading|setup)\.html$/.test(file)
|| /^frontends\/desktop\/(?:vite\.config\.[^/]+|tsconfig(?:\.[^/]+)?\.json|package-lock\.json)$/.test(file)
));
if (forbidden.length > 0) {
fail(`tracked React development files crossed the compiled-only boundary: ${forbidden.join(', ')}`);
}
}
async function verifyNoticeAttributes() {
const attributes = await readFile(path.join(repoRoot, '.gitattributes'), 'utf8');
const lines = attributes.split(/\r?\n/);
for (const rule of noticeAttributeRules) {
const noticePath = rule.split(/\s+/, 1)[0];
const matchingRules = lines.filter((line) => line.trim().split(/\s+/, 1)[0] === noticePath);
if (matchingRules.length !== 1 || matchingRules[0] !== rule) {
fail(`third-party notice must have one exact LF attribute rule: ${rule}`);
}
}
}
async function verifyVersionsRendererAndEntry() {
const packageJson = await readJson(path.join(desktopDir, 'package.json'));
const tauriConfig = await readJson(path.join(desktopDir, 'src-tauri', 'tauri.conf.json'));
const tauriE2eConfig = await readJson(path.join(desktopDir, 'src-tauri', 'tauri.e2e.conf.json'));
const cargoToml = await readFile(path.join(desktopDir, 'src-tauri', 'Cargo.toml'), 'utf8');
const cargoLock = await readFile(path.join(desktopDir, 'src-tauri', 'Cargo.lock'), 'utf8');
const desktopShell = await readFile(path.join(desktopDir, 'src-tauri', 'src', 'lib.rs'), 'utf8');
if (packageJson.version !== expectedVersion) fail(`package.json version must be ${expectedVersion}`);
if (tauriConfig.version !== expectedVersion) fail(`tauri.conf.json version must be ${expectedVersion}`);
if (tauriConfig.build?.frontendDist !== '../dist') fail('Tauri frontendDist must be ../dist');
for (const sourceBuildKey of ['beforeBuildCommand', 'beforeDevCommand', 'devUrl']) {
if (sourceBuildKey in tauriConfig.build) fail(`compiled-only Tauri config must not require ${sourceBuildKey}`);
}
if (!/^\[package\][\s\S]*?^name = "ga-desktop"$[\s\S]*?^version = "0\.2\.1"$/m.test(cargoToml)) {
fail(`Cargo.toml ga-desktop version must be ${expectedVersion}`);
}
if (!/^\[\[package\]\][\s\S]*?^name = "ga-desktop"$\nversion = "0\.2\.1"$/m.test(cargoLock)) {
fail(`Cargo.lock ga-desktop version must be ${expectedVersion}`);
}
const productionSecurity = tauriConfig.app?.security;
if (!productionSecurity?.csp || typeof productionSecurity.csp !== 'object') {
fail('production renderer must use an explicit CSP');
}
if (String(productionSecurity.csp['script-src']).includes('unsafe-eval')) {
fail('production CSP must not enable unsafe-eval');
}
if (!String(productionSecurity.csp['connect-src']).includes('127.0.0.1:14168')) {
fail('production CSP must retain the Desktop loopback bridge');
}
if (JSON.stringify(productionSecurity).includes('wdio:')) {
fail('production Tauri security config must not grant WebDriver permissions');
}
if (!JSON.stringify(tauriE2eConfig).includes('wdio:default')) {
fail('E2E-only Tauri config must retain its isolated WebDriver capability');
}
if (!desktopShell.includes('fn main_ui_url_from_current')) {
fail('desktop shell must resolve index.html from the active Tauri asset origin');
}
if (/tauri::Url::parse\("http:\/\/127\.0\.0\.1:14168\/?"\)/.test(desktopShell)) {
fail('desktop shell must not navigate the renderer to the legacy bridge root');
}
}
async function verifyReleaseContract() {
const workflow = await readFile(
path.join(repoRoot, '.github', 'workflows', 'desktop-release-package.yml'),
'utf8',
);
const buildJobs = ['build-windows', 'build-linux', 'build-macos'];
const tauriCommands = {
'build-windows': 'npm run tauri build -- --bundles nsis',
'build-linux': 'npm run tauri build -- --bundles appimage',
'build-macos': 'npm run tauri build -- --bundles app',
};
const jobNames = [...workflow.slice(workflow.search(/^jobs:\s*$/m)).matchAll(/^ ([a-zA-Z0-9_-]+):\s*$/gm)]
.map((match) => match[1]);
if (JSON.stringify(jobNames) !== JSON.stringify([...buildJobs, 'publish-release'])) {
fail('release workflow must contain exactly three builders and one publisher');
}
if (!/^permissions:\n contents: read\s*$/m.test(workflow)) {
fail('release workflow must default to contents: read');
}
if ((workflow.match(/^ contents: write\s*$/gm) ?? []).length !== 1) {
fail('release workflow must grant contents: write exactly once');
}
if ((workflow.match(/^ run: npm run test:dist$/gm) ?? []).length !== 3) {
fail('all three builders must run the compiled renderer byte contract');
}
if (/\bnpm ci\b|\bnpm run build\b|\b(?:npx|npm exec)\s+vite\b/.test(workflow)) {
fail('compiled-only release workflow must not require the React source toolchain');
}
for (const job of buildJobs) {
const body = workflowJob(workflow, job);
if (!body) fail(`missing release builder: ${job}`);
if (!/^ permissions:\n contents: read\s*$/m.test(body) || body.includes('contents: write')) {
fail(`${job} must have read-only repository permission`);
}
if (!/uses: actions\/checkout@[0-9a-f]{40}[^\n]*\n with:\n persist-credentials: false/.test(body)) {
fail(`${job} checkout must use a full SHA and disable persisted credentials`);
}
if (body.includes('secrets.GITHUB_TOKEN') || /\bgh release\b/.test(body)) {
fail(`${job} must not receive a release token or publish`);
}
if (!body.includes('npm install --package-lock=false')
|| !body.includes("if(v!=='2.11.4')")
|| !body.includes('test ! -e package-lock.json')
|| body.includes('npm ci')) {
fail(`${job} must install only the submitted minimal Tauri package manifest`);
}
const adjacentGate = new RegExp(
' - name: Verify tracked compiled renderer bytes\\n'
+ ' working-directory: frontends/desktop\\n'
+ ' run: npm run test:dist\\n\\n'
+ ' - name: [^\\n]+\\n'
+ ' working-directory: frontends/desktop\\n'
+ ` run: ${tauriCommands[job].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`,
);
if (!adjacentGate.test(body)) {
fail(`${job} must validate notice bytes immediately before Tauri embeds dist`);
}
if (!(body.includes("--exclude='./frontends/desktop/dist'")
|| body.includes("--exclude='frontends/desktop/dist'"))) {
fail(`${job} must not duplicate Tauri-embedded dist inside runtime/app`);
}
if (!/--exclude='(?:\.\/)?frontends\/desktop\/release_qualification'/.test(body)) {
fail(`${job} must not package release qualification tooling inside runtime/app`);
}
if (!(body.includes('test ! -e "$RUNTIME/app/frontends/desktop/dist"')
|| body.includes('test ! -e "$RUNTIME_SRC/app/frontends/desktop/dist"'))) {
fail(`${job} must verify dist is not duplicated inside runtime/app`);
}
}
const windows = workflowJob(workflow, 'build-windows');
if (!windows.includes('cygpath -u "$RUNNER_TEMP"')
|| !windows.includes('PBS_ARCHIVE="${RUNNER_TEMP_POSIX}/pbs-windows-x86_64.tar.gz"')) {
fail('Windows packaging must convert RUNNER_TEMP with cygpath before POSIX tools use it');
}
const macos = workflowJob(workflow, 'build-macos');
if (!macos.includes('runs-on: macos-26')
|| !macos.includes('DEVELOPER_DIR: /Applications/Xcode_26.5.app/Contents/Developer')
|| !macos.includes('test "$(uname -m)" = arm64')
|| !macos.includes('test "$(xcodebuild -version | sed -n \'1p\')" = "Xcode 26.5"')
|| !macos.includes('test "$(xcodebuild -version | sed -n \'2p\')" = "Build version 17F42"')
|| !macos.includes('test "$(xcrun --sdk macosx --show-sdk-version)" = "26.5"')
|| !workflow.includes('MACOS_PACKAGING_PYTHON_VERSION: "3.12.10"')
|| !workflow.includes('PBS_PYTHON_VERSION: "3.12.14"')) {
fail('macOS packaging must pin macos-26, Xcode 26.5 build 17F42, SDK 26.5, arm64, and separate Python inputs');
}
const linux = workflowJob(workflow, 'build-linux');
if (!linux.includes('runs-on: ubuntu-22.04')
|| !linux.includes('prefix-key: "v1-rust-release-ubuntu-22.04-glibc-2.35"')
|| linux.includes('prefix-key: "v0-rust"')
|| !linux.includes('cache-targets: "false"')
|| !linux.includes('cache-bin: "false"')) {
fail('Linux packaging must isolate its Ubuntu 22.04/glibc 2.35 Rust cache ABI');
}
const publisher = workflowJob(workflow, 'publish-release');
if (!publisher.includes('needs: [build-windows, build-linux, build-macos]')
|| !publisher.includes("github.event_name == 'push'")
|| !publisher.includes('refs/tags/desktop-portable-')
|| !/^ permissions:\n contents: write\s*$/m.test(publisher)) {
fail('publisher must be the sole tag-only writer after all three builders');
}
const publisherRun = publisher.slice(publisher.indexOf(' run: |'));
if (!publisher.includes('TAG_NAME: ${{ github.ref_name }}')
|| publisherRun.includes('${{ github.ref_name }}')
|| !publisherRun.includes('^desktop-portable-[A-Za-z0-9._-]+$')) {
fail('publisher must pass the tag through env and validate it before shell use');
}
if ((publisher.match(/actions\/download-artifact@/g) ?? []).length !== 3
|| (publisher.match(/\bgh release create\b/g) ?? []).length !== 1
|| publisher.includes('gh release upload')
|| !publisher.includes('--draft')
|| !publisher.includes('gh release edit "$TAG_NAME" --draft=false --prerelease')) {
fail('publisher must aggregate three artifacts as a verified draft, then expose one prerelease');
}
for (const file of [
'GenericAgent-Desktop-Windows-Portable.zip',
'SHA256SUMS-windows.txt',
'GenericAgent-Desktop-Linux-Portable.tar.gz',
'SHA256SUMS-linux.txt',
'GenericAgent-Desktop-macOS-aarch64.dmg',
'GenericAgent-Desktop-macOS-aarch64.dmg.sha256',
]) {
if (!publisher.includes(file)) fail(`publisher does not require release asset: ${file}`);
}
}
async function verifyGeneratedManifest(files) {
const provenance = await readJson(path.join(distDir, 'build-provenance.json'));
const generatedFiles = files
.map((file) => ({ file, relativePath: relativeToDist(file) }))
.filter(({ relativePath }) => !excludedManifestFiles.has(relativePath))
.sort((left, right) => left.relativePath.localeCompare(right.relativePath, 'en'));
const manifestLines = [];
for (const { file, relativePath } of generatedFiles) {
manifestLines.push(`${sha256(await readFile(file))} ${relativePath}\n`);
}
const manifestDigest = sha256(manifestLines.join(''));
if (provenance.version !== expectedVersion) fail(`provenance version must be ${expectedVersion}`);
if (provenance.sourceRepository !== 'https://github.com/abraxas914/GenericAgent') {
fail('provenance source repository is incorrect');
}
if (provenance.sourceCommit !== expectedSourceCommit) fail('provenance source commit is incorrect');
if (provenance.generatedAssetCount !== generatedFiles.length) {
fail(`provenance asset count mismatch: expected ${provenance.generatedAssetCount}, found ${generatedFiles.length}`);
}
if (provenance.generatedManifestSha256 !== manifestDigest) {
fail(`provenance manifest mismatch: expected ${provenance.generatedManifestSha256}, found ${manifestDigest}`);
}
}
async function main() {
const requiredFiles = [
'index.html',
'loading.html',
'setup.html',
'fallback.html',
'assets/ga-logo.svg',
'THIRD_PARTY_NOTICES.txt',
'README.md',
'build-provenance.json',
];
await Promise.all(requiredFiles.map(assertExists));
for (const deadAsset of removedLegacyAssets) {
if (await exists(path.join(distDir, deadAsset))) {
fail(`dead legacy React asset remains in compiled output: ${deadAsset}`);
}
}
verifyTrackedSourceBoundary();
await verifyNoticeAttributes();
const files = await walk(distDir);
const forbiddenSource = files.map(relativeToDist).filter((file) => /(?:\.map|\.tsx?|\.jsx)$/i.test(file));
if (forbiddenSource.length > 0) fail(`compiled distribution contains source files: ${forbiddenSource.join(', ')}`);
const searchableFiles = files.filter((file) => /\.(?:html|js|css)$/i.test(file));
const searchableContents = [];
for (const file of searchableFiles) {
const contents = await readFile(file, 'utf8');
searchableContents.push(contents);
if (/(?:webdriverio|__GA_E2E__|wdio:|sourceMappingURL|webpack:\/\/|\/Users\/)/i.test(contents)) {
fail(`compiled distribution leaks source/E2E material: ${relativeToDist(file)}`);
}
}
const compiledRenderer = searchableContents.join('\n');
for (const marker of [
'services/capabilities',
'data-import-row',
'data-export-row',
'get_macos_titlebar_metrics',
'titlebar-controls',
]) {
if (!compiledRenderer.includes(marker)) {
fail(`compiled distribution is missing the v0.2.1 renderer contract: ${marker}`);
}
}
const notice = await readFile(path.join(distDir, 'THIRD_PARTY_NOTICES.txt'));
if (sha256(notice) !== expectedNoticeSha256) fail('Semi Design third-party notice hash is incorrect');
const readme = await readFile(path.join(distDir, 'README.md'), 'utf8');
for (const value of [
expectedSourceCommit,
...semiPackages,
'https://github.com/DouyinFE/semi-design',
'https://semi.design',
'Copyright (c) 2021 DouyinFE',
'License: MIT',
'THIRD_PARTY_NOTICES.txt',
'5ddf03bb152666637bdfcfa44f1fac3cff5a66b6',
'2fb55d944e4444b08cb9ad76c13aef7a5788186b',
'3e7ca6a2b20eefdb3ee335dc0d520c3b6d9d57f8',
'not a claim that later builds are bit-for-bit reproducible',
]) {
if (!readme.includes(value)) fail(`compiled README is missing required provenance/license text: ${value}`);
}
await Promise.all(['index.html', 'loading.html', 'setup.html'].map(verifyHtmlReferences));
await verifyVersionsRendererAndEntry();
await verifyReleaseContract();
await verifyGeneratedManifest(files);
console.log('Compiled React Desktop 2.0 distribution contracts passed.');
}
await main();