Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/browseros-agent/.fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { installNativeAddonGuard } from './lib/native-addon-guard'

installNativeAddonGuard()
await import('./index')
17 changes: 0 additions & 17 deletions packages/browseros-agent/apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()

Expand Down
19 changes: 19 additions & 0 deletions packages/browseros-agent/apps/server/src/lib/native-addon-guard.ts
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 4 additions & 3 deletions packages/browseros-agent/apps/server/tests/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
Expand All @@ -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 () => {
Expand Down
8 changes: 3 additions & 5 deletions packages/browseros-agent/scripts/build/server/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
)
Comment on lines +8 to +11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer import.meta.dir over process.cwd() so the path resolves correctly regardless of which working directory the test runner is launched from. If bun test is invoked from the repo root, process.cwd() won't point to packages/browseros-agent and every subprocess-spawning test in this file will fail with a module-not-found error.

Suggested change
const nativeAddonGuardPath = join(
process.cwd(),
'apps/server/src/lib/native-addon-guard.ts',
)
const nativeAddonGuardPath = join(
import.meta.dir,
'../../../apps/server/src/lib/native-addon-guard.ts',
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/scripts/build/server/native-addon-policy.test.ts
Line: 8-11

Comment:
Prefer `import.meta.dir` over `process.cwd()` so the path resolves correctly regardless of which working directory the test runner is launched from. If `bun test` is invoked from the repo root, `process.cwd()` won't point to `packages/browseros-agent` and every subprocess-spawning test in this file will fail with a module-not-found error.

```suggestion
const nativeAddonGuardPath = join(
  import.meta.dir,
  '../../../apps/server/src/lib/native-addon-guard.ts',
)
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


describe('compiled server native addon policy', () => {
let tempDir: string | null = null
Expand All @@ -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 () => {
Expand All @@ -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) {',
Expand All @@ -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',
Expand Down Expand Up @@ -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')
})
})
Expand All @@ -106,3 +132,8 @@ async function collectProcess(process: CollectableProcess) {

return { stdout, stderr, exitCode }
}

async function listFiles(dir: string): Promise<string[]> {
const entries = await readdir(dir, { recursive: true })
return entries.map(String).sort()
}
Loading