diff --git a/packages/browseros-agent/.fallowrc.json b/packages/browseros-agent/.fallowrc.json index b2cf34797..02dbebd41 100644 --- a/packages/browseros-agent/.fallowrc.json +++ b/packages/browseros-agent/.fallowrc.json @@ -2,6 +2,7 @@ "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", "entry": [ "apps/server/src/index.ts", + "apps/server/src/compiled-bootstrap.ts", "apps/agent/entrypoints/app/main.tsx", "apps/agent/entrypoints/sidepanel/main.tsx", "apps/agent/entrypoints/background/index.ts", diff --git a/packages/browseros-agent/apps/server/src/compiled-bootstrap.ts b/packages/browseros-agent/apps/server/src/compiled-bootstrap.ts new file mode 100644 index 000000000..1c488a5b7 --- /dev/null +++ b/packages/browseros-agent/apps/server/src/compiled-bootstrap.ts @@ -0,0 +1,4 @@ +import { installNativeAddonGuard } from './lib/native-addon-guard' + +installNativeAddonGuard() +await import('./index') diff --git a/packages/browseros-agent/apps/server/src/index.ts b/packages/browseros-agent/apps/server/src/index.ts index 9eb2be49e..48990b3db 100755 --- a/packages/browseros-agent/apps/server/src/index.ts +++ b/packages/browseros-agent/apps/server/src/index.ts @@ -6,7 +6,6 @@ * BrowserOS Server - Entry Point */ -// Runtime check for Bun if (typeof Bun === 'undefined') { console.error('Error: This application requires Bun runtime.') console.error( @@ -23,22 +22,6 @@ import { loadServerConfig } from './config' import { isPortInUseError } from './lib/port-binding' import { Sentry } from './lib/sentry' import { Application } from './main' -import { VERSION } from './version' - -/** Handles app flags passed after Bun's compiled-runtime separator. */ -function isCompiledVersionRequest(argv: string[]): boolean { - return ( - process.execArgv.includes('--no-addons') && - argv.length === 4 && - argv[2] === '--' && - argv[3] === '--version' - ) -} - -if (isCompiledVersionRequest(process.argv)) { - console.log(VERSION) - process.exit(0) -} const configResult = loadServerConfig() diff --git a/packages/browseros-agent/apps/server/src/lib/native-addon-guard.ts b/packages/browseros-agent/apps/server/src/lib/native-addon-guard.ts new file mode 100644 index 000000000..6a2f20622 --- /dev/null +++ b/packages/browseros-agent/apps/server/src/lib/native-addon-guard.ts @@ -0,0 +1,19 @@ +const NATIVE_ADDON_DISABLED_MESSAGE = + 'BrowserOS server disables native addon loading in compiled production builds' + +interface GuardedProcess extends NodeJS.Process { + __browserosNativeAddonGuardInstalled?: boolean +} + +/** Blocks native addons before Bun can extract bundled `.node` files. */ +export function installNativeAddonGuard(): void { + const guardedProcess = process as GuardedProcess + if (guardedProcess.__browserosNativeAddonGuardInstalled) return + + const guard: NodeJS.Process['dlopen'] = () => { + throw new Error(NATIVE_ADDON_DISABLED_MESSAGE) + } + + process.dlopen = guard + guardedProcess.__browserosNativeAddonGuardInstalled = true +} diff --git a/packages/browseros-agent/apps/server/tests/build.test.ts b/packages/browseros-agent/apps/server/tests/build.test.ts index 3f991ec6d..74d4b4b41 100644 --- a/packages/browseros-agent/apps/server/tests/build.test.ts +++ b/packages/browseros-agent/apps/server/tests/build.test.ts @@ -136,8 +136,7 @@ describe('server build', () => { assert.fail(`Build failed (exit ${buildExit}):\n${stderr}`) } - // Embedded Bun exec argv consumes bare runtime-looking flags. - const proc = Bun.spawn([binaryPath, '--', '--version'], { + const proc = Bun.spawn([binaryPath, '--version'], { stdout: 'pipe', stderr: 'pipe', }) @@ -152,7 +151,9 @@ describe('server build', () => { 0, `Binary --version exited non-zero:\n${versionStderr}`, ) - assert.strictEqual(versionOutput.trim(), expectedVersion) + const actualVersion = versionOutput.trim() + assert.strictEqual(actualVersion, expectedVersion) + assert.notStrictEqual(actualVersion, Bun.version) }, 300_000) it('archives CI builds without R2 config or production env secrets', async () => { diff --git a/packages/browseros-agent/scripts/build/server/compile.ts b/packages/browseros-agent/scripts/build/server/compile.ts index 4db29724c..28cccf370 100644 --- a/packages/browseros-agent/scripts/build/server/compile.ts +++ b/packages/browseros-agent/scripts/build/server/compile.ts @@ -9,11 +9,10 @@ import type { BuildTarget, CompiledServerBinary } from './types' const DIST_PROD_ROOT = 'dist/prod/server' const TMP_ROOT = join(DIST_PROD_ROOT, '.tmp') const BUNDLE_DIR = join(TMP_ROOT, 'bundle') -const BUNDLE_ENTRY = join(BUNDLE_DIR, 'index.js') +const BUNDLE_ENTRY = join(BUNDLE_DIR, 'compiled-bootstrap.js') const BINARIES_DIR = join(TMP_ROOT, 'binaries') -// Bun embeds native addons by extracting hidden temp `.node` files at runtime. -export const COMPILED_SERVER_EXEC_ARGV = ['--no-addons'] +export const SERVER_BUNDLE_ENTRYPOINT = 'apps/server/src/compiled-bootstrap.ts' function compiledBinaryPath(target: BuildTarget): string { return join( @@ -30,7 +29,7 @@ async function bundleServer( mkdirSync(BUNDLE_DIR, { recursive: true }) const result = await Bun.build({ - entrypoints: ['apps/server/src/index.ts'], + entrypoints: [SERVER_BUNDLE_ENTRYPOINT], outdir: BUNDLE_DIR, target: 'bun', minify: true, @@ -66,7 +65,6 @@ async function compileTarget( '--outfile', binaryPath, `--target=${target.bunTarget}`, - `--compile-exec-argv=${COMPILED_SERVER_EXEC_ARGV.join(' ')}`, '--external=node-pty', ] await runCommand('bun', args, env) diff --git a/packages/browseros-agent/scripts/build/server/native-addon-policy.test.ts b/packages/browseros-agent/scripts/build/server/native-addon-policy.test.ts index 02e86b8c9..2c3ae1bda 100644 --- a/packages/browseros-agent/scripts/build/server/native-addon-policy.test.ts +++ b/packages/browseros-agent/scripts/build/server/native-addon-policy.test.ts @@ -1,9 +1,14 @@ import { afterEach, describe, expect, it } from 'bun:test' -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { COMPILED_SERVER_EXEC_ARGV } from './compile' +import { SERVER_BUNDLE_ENTRYPOINT } from './compile' + +const nativeAddonGuardPath = join( + process.cwd(), + 'apps/server/src/lib/native-addon-guard.ts', +) describe('compiled server native addon policy', () => { let tempDir: string | null = null @@ -15,8 +20,34 @@ describe('compiled server native addon policy', () => { } }) - it('bakes native-addon loading disablement into compiled server binaries', () => { - expect(COMPILED_SERVER_EXEC_ARGV).toContain('--no-addons') + it('bundles the compiled bootstrap entrypoint', () => { + expect(SERVER_BUNDLE_ENTRYPOINT).toBe( + 'apps/server/src/compiled-bootstrap.ts', + ) + }) + + it('installs the native-addon guard idempotently', async () => { + tempDir = await mkdtemp(join(tmpdir(), 'browseros-native-addon-policy-')) + const sourcePath = join(tempDir, 'idempotent.ts') + await writeFile( + sourcePath, + [ + `import { installNativeAddonGuard } from ${JSON.stringify(nativeAddonGuardPath)}`, + 'installNativeAddonGuard()', + 'const guarded = process.dlopen', + 'installNativeAddonGuard()', + 'console.log(String(process.dlopen === guarded))', + ].join('\n'), + ) + + const result = await collectProcess( + Bun.spawn(['bun', sourcePath], { + stdout: 'pipe', + stderr: 'pipe', + }), + ) + + expect(result).toMatchObject({ exitCode: 0, stdout: 'true\n' }) }) it('prevents Bun from opening hidden temp native addons', async () => { @@ -32,6 +63,8 @@ describe('compiled server native addon policy', () => { await writeFile( sourcePath, [ + `import { installNativeAddonGuard } from ${JSON.stringify(nativeAddonGuardPath)}`, + 'installNativeAddonGuard()', 'try {', ' require("./addon.node")', '} catch (error) {', @@ -42,15 +75,7 @@ describe('compiled server native addon policy', () => { ) const build = Bun.spawn( - [ - 'bun', - 'build', - '--compile', - `--compile-exec-argv=${COMPILED_SERVER_EXEC_ARGV.join(' ')}`, - sourcePath, - '--outfile', - binaryPath, - ], + ['bun', 'build', '--compile', sourcePath, '--outfile', binaryPath], { stdout: 'pipe', stderr: 'pipe', @@ -85,8 +110,9 @@ describe('compiled server native addon policy', () => { const appResult = await collectProcess(app) expect(appResult.stderr).toContain( - 'Cannot load native addon because loading addons is disabled', + 'BrowserOS server disables native addon loading in compiled production builds', ) + expect(await listFiles(runTmpDir)).toEqual([]) expect(openFiles.stdout).not.toContain('.node') }) }) @@ -106,3 +132,8 @@ async function collectProcess(process: CollectableProcess) { return { stdout, stderr, exitCode } } + +async function listFiles(dir: string): Promise { + const entries = await readdir(dir, { recursive: true }) + return entries.map(String).sort() +}