From ca4a05a5dfeec72fc72275473fdf539489397323 Mon Sep 17 00:00:00 2001 From: m4air Date: Mon, 28 Sep 2026 17:32:24 -0700 Subject: [PATCH 1/3] fix(ssh): package and verify the Windows terminal provider before relay startup --- config/scripts/build-relay.mjs | 2 + config/scripts/build-windows-conpty.test.mjs | 62 ++++++++++++- .../scripts/relay-artifact-manifest.test.mjs | 13 +++ config/scripts/relay-conpty-packaging.mjs | 25 +++++ .../ssh/ssh-relay-deploy-windows-pipe.test.ts | 3 + src/main/ssh/ssh-relay-deploy.ts | 3 +- src/relay/relay.ts | 2 + .../windows-relay-conpty-admission.test.ts | 93 +++++++++++++++++++ src/relay/windows-relay-conpty-admission.ts | 29 ++++++ src/shared/relay-artifacts.ts | 8 ++ src/shared/relay-optional-artifacts.test.ts | 28 ++++++ 11 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 config/scripts/relay-conpty-packaging.mjs create mode 100644 src/relay/windows-relay-conpty-admission.test.ts create mode 100644 src/relay/windows-relay-conpty-admission.ts diff --git a/config/scripts/build-relay.mjs b/config/scripts/build-relay.mjs index 88ddd9c11c37..8578861b9e57 100644 --- a/config/scripts/build-relay.mjs +++ b/config/scripts/build-relay.mjs @@ -3,6 +3,7 @@ import { build } from 'esbuild' import { bundledParcelWatcherPlugin } from './parcel-watcher-bundle.mjs' import { readWindowsWatcherArtifact } from './windows-watcher-artifact.mjs' +import { stageWindowsRelayConpty } from './relay-conpty-packaging.mjs' import { createHash } from 'node:crypto' import { copyFileSync, @@ -107,6 +108,7 @@ for (const platform of RELAY_BUILD_PLATFORMS) { const watcher = readWindowsWatcherArtifact(platform.slice('win32-'.length)) copyFileSync(watcher.binary, join(outDir, RELAY_WINDOWS_WATCHER_FILENAME)) copyFileSync(watcher.license, join(outDir, RELAY_WINDOWS_WATCHER_LICENSE)) + await stageWindowsRelayConpty(platform, outDir) } await build({ diff --git a/config/scripts/build-windows-conpty.test.mjs b/config/scripts/build-windows-conpty.test.mjs index 7d50fa136455..ebc3331a253c 100644 --- a/config/scripts/build-windows-conpty.test.mjs +++ b/config/scripts/build-windows-conpty.test.mjs @@ -1,5 +1,13 @@ import { createHash } from 'node:crypto' -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync +} from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -23,6 +31,8 @@ vi.mock('./zip-extractor-command.mjs', () => ({ })) vi.mock('./script-child-process.mjs', () => ({ runProcessSync: vi.fn() })) import { runProcessSync } from './script-child-process.mjs' +import { stageWindowsRelayConpty } from './relay-conpty-packaging.mjs' +import { RELAY_WINDOWS_CONPTY_FILENAMES } from '../../src/shared/relay-artifacts.ts' import { downloadConptyArchive, materializeWindowsConpty, @@ -145,3 +155,53 @@ describe('independent ConPTY payload builder', () => { await expect(downloadConptyArchive(join(root, 'download'), fetcher)).rejects.toThrow('HTTP 404') }) }) + +describe('Windows relay provider packaging', () => { + it.each(['x64', 'arm64'])('stages the verified %s pair and metadata flat', async (arch) => { + mkdirSync(output) + await stageWindowsRelayConpty(`win32-${arch}`, output, { cacheDir, fetcher }) + expect(readdirSync(output).sort()).toEqual([...RELAY_WINDOWS_CONPTY_FILENAMES].sort()) + verifyConptyDirectory(output, arch) + expect(readFileSync(join(output, 'conpty-LICENSE.txt'), 'utf8')).toContain('MIT License') + expect(JSON.parse(readFileSync(join(output, 'conpty.json'), 'utf8'))).toMatchObject({ + arch, + archiveSha256: fixture.archiveHash, + files: fixture.files[arch] + }) + }) + + it.each(['conpty.dll', 'OpenConsole.exe'])( + 'rejects corrupt %s and removes staging', + async (filename) => { + mkdirSync(output) + fixture.files.x64[filename] = digest('wrong binary') + await expect( + stageWindowsRelayConpty('win32-x64', output, { cacheDir, fetcher }) + ).rejects.toThrow('checksum mismatch') + expect(readdirSync(output)).toEqual([]) + } + ) + + it('rejects a missing companion and removes staging', async () => { + mkdirSync(output) + const extract = vi.mocked(runProcessSync).getMockImplementation() + vi.mocked(runProcessSync).mockImplementation((command) => { + const result = extract(command) + rmSync(join(command.args[1], 'build', 'native', 'runtimes', 'x64', 'OpenConsole.exe')) + return result + }) + await expect( + stageWindowsRelayConpty('win32-x64', output, { cacheDir, fetcher }) + ).rejects.toThrow() + expect(readdirSync(output)).toEqual([]) + }) + + it.each(['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'])( + 'does not touch %s packaging', + async (platform) => { + await stageWindowsRelayConpty(platform, output, { cacheDir, fetcher }) + expect(existsSync(output)).toBe(false) + expect(fetcher).not.toHaveBeenCalled() + } + ) +}) diff --git a/config/scripts/relay-artifact-manifest.test.mjs b/config/scripts/relay-artifact-manifest.test.mjs index 7177b9821f8f..fba7a91e9851 100644 --- a/config/scripts/relay-artifact-manifest.test.mjs +++ b/config/scripts/relay-artifact-manifest.test.mjs @@ -12,11 +12,14 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { RELAY_BUILD_PLATFORMS, RELAY_VERSION_FILENAME, + RELAY_WINDOWS_CONPTY_FILENAMES, isWindowsRelayPlatform, relayArtifactFilenames, relayOptionalArtifactFilenames } from '../../src/shared/relay-artifacts.ts' +import { verifyConptyDirectory } from './build-windows-conpty.mjs' + const projectDir = resolve(import.meta.dirname, '../..') // Its own tree: building into out/relay would clobber a developer's build and // race the suites that read it. @@ -35,6 +38,16 @@ afterAll(() => { }) describe('packaged relay artifact manifest', () => { + it.each(['x64', 'arm64'])('ships the pinned %s provider as required flat artifacts', (arch) => { + const outDir = join(relayOutDir, `win32-${arch}`) + verifyConptyDirectory(outDir, arch) + for (const filename of RELAY_WINDOWS_CONPTY_FILENAMES) { + expect(relayArtifactFilenames(true)).toContain(filename) + expect(relayArtifactFilenames(false)).not.toContain(filename) + expect(relayOptionalArtifactFilenames(true)).not.toContain(filename) + } + }) + it.each([...RELAY_BUILD_PLATFORMS])('emits exactly the declared artifacts for %s', (platform) => { const outDir = join(relayOutDir, platform) const expected = relayArtifactFilenames(isWindowsRelayPlatform(platform)) diff --git a/config/scripts/relay-conpty-packaging.mjs b/config/scripts/relay-conpty-packaging.mjs new file mode 100644 index 000000000000..6ff8913055fb --- /dev/null +++ b/config/scripts/relay-conpty-packaging.mjs @@ -0,0 +1,25 @@ +import { copyFileSync, mkdtempSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { + isWindowsRelayPlatform, + RELAY_WINDOWS_CONPTY_FILENAMES +} from '../../src/shared/relay-artifacts.ts' +import { materializeWindowsConpty, verifyConptyDirectory } from './build-windows-conpty.mjs' + +export async function stageWindowsRelayConpty(platform, outputDir, options = {}) { + if (!isWindowsRelayPlatform(platform)) { + return + } + const arch = platform.slice('win32-'.length) + const staging = mkdtempSync(join(outputDir, '.conpty-')) + try { + await materializeWindowsConpty(arch, staging, options) + for (const filename of RELAY_WINDOWS_CONPTY_FILENAMES) { + const source = filename === 'conpty-LICENSE.txt' ? 'LICENSE.txt' : filename + copyFileSync(join(staging, source), join(outputDir, filename)) + } + verifyConptyDirectory(outputDir, arch) + } finally { + rmSync(staging, { recursive: true, force: true }) + } +} diff --git a/src/main/ssh/ssh-relay-deploy-windows-pipe.test.ts b/src/main/ssh/ssh-relay-deploy-windows-pipe.test.ts index 7fe1140306b3..fac1dee16e57 100644 --- a/src/main/ssh/ssh-relay-deploy-windows-pipe.test.ts +++ b/src/main/ssh/ssh-relay-deploy-windows-pipe.test.ts @@ -195,6 +195,9 @@ describe('deployAndLaunchRelay on Windows remotes', () => { expect(launchScript).toContain('--endpoint-dir') expect(launchScript).toContain("& 'C:/Program Files/Orca/bun.exe'") expect(launchScript).toContain('--launch-error-file') + expect(launchScript).toContain( + "$env:BUN_CONPTY_LIBRARY = 'C:/Users/me user/.orca-remote/relay-0.1.0+abcdef012345/conpty.dll'; & 'C:/Program Files/Orca/bun.exe'" + ) expect(launchScript).not.toMatch(/Invoke-CimMethod|cmd\.exe|\$env:PATH/) expect(launchScript).toContain('Set-Location') expect(launchScript).toContain( diff --git a/src/main/ssh/ssh-relay-deploy.ts b/src/main/ssh/ssh-relay-deploy.ts index ae37470dae12..93b8d6a050bd 100644 --- a/src/main/ssh/ssh-relay-deploy.ts +++ b/src/main/ssh/ssh-relay-deploy.ts @@ -1485,6 +1485,7 @@ function windowsRelayLaunchCommand( ripgrepPath?: string ): string { const relayScript = joinRemotePath(hostPlatform, remoteDir, 'relay.js') + const provider = joinRemotePath(hostPlatform, remoteDir, 'conpty.dll') const args = [ '--no-env-file', '--config=NUL', @@ -1508,7 +1509,7 @@ function windowsRelayLaunchCommand( return commandInRemoteDirectory( hostPlatform, remoteDir, - `& ${powerShellLiteral(nodePath)} ${args.map(powerShellLiteral).join(' ')}; if ($LASTEXITCODE -ne 0) { throw "Bun detached launch failed with exit $LASTEXITCODE" }` + `$env:BUN_CONPTY_LIBRARY = ${powerShellLiteral(provider)}; & ${powerShellLiteral(nodePath)} ${args.map(powerShellLiteral).join(' ')}; if ($LASTEXITCODE -ne 0) { throw "Bun detached launch failed with exit $LASTEXITCODE" }` ) } diff --git a/src/relay/relay.ts b/src/relay/relay.ts index 8919a87d8b4f..6244cc405e66 100644 --- a/src/relay/relay.ts +++ b/src/relay/relay.ts @@ -11,9 +11,11 @@ import { runRelayDaemon } from './relay-daemon' import { relayLogLine } from './relay-diagnostic-log' import { configureRelayBundledRipgrep } from './relay-bundled-ripgrep' import { launchDetachedWindowsRelay } from './windows-detached-launch' +import { assertWindowsRelayConptyProvider } from './windows-relay-conpty-admission' async function main(): Promise { const options = parseRelayLaunchOptions(process.argv) + assertWindowsRelayConptyProvider(options) if (options.connectMode) { runRelayConnectChannel(options.sockPath, readRelayEndpointCredential(options.credentialFile)) return diff --git a/src/relay/windows-relay-conpty-admission.test.ts b/src/relay/windows-relay-conpty-admission.test.ts new file mode 100644 index 000000000000..dac35078bf9f --- /dev/null +++ b/src/relay/windows-relay-conpty-admission.test.ts @@ -0,0 +1,93 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { assertWindowsRelayConptyProvider } from './windows-relay-conpty-admission' + +vi.mock('../shared/windows-conpty-release', async () => { + const { createHash } = await import('node:crypto') + return { + WINDOWS_CONPTY_FILES: Object.fromEntries( + ['x64', 'arm64'].map((arch) => [ + arch, + Object.fromEntries( + ['conpty.dll', 'OpenConsole.exe'].map((file) => [ + file, + createHash('sha256').update(`${arch}/${file}`).digest('hex') + ]) + ) + ]) + ) + } +}) +const platform = Object.getOwnPropertyDescriptor(process, 'platform')! +const arch = Object.getOwnPropertyDescriptor(process, 'arch')! +const argv = process.argv +const roots: string[] = [] +const daemon = { connectMode: false, cliMode: false } +beforeEach(() => { + Object.defineProperty(process, 'platform', { value: 'win32' }) + Object.defineProperty(process, 'arch', { value: 'x64' }) +}) +afterEach(() => { + Object.defineProperty(process, 'platform', platform) + Object.defineProperty(process, 'arch', arch) + process.argv = argv + vi.unstubAllEnvs() + roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })) +}) +function fixture(architecture = 'x64'): string { + const root = mkdtempSync(join(tmpdir(), 'orca-relay-conpty-')) + roots.push(root) + writeFileSync(join(root, 'relay.js'), '') + for (const file of ['conpty.dll', 'OpenConsole.exe']) { + writeFileSync(join(root, file), `${architecture}/${file}`) + } + process.argv = [process.execPath, join(root, 'relay.js')] + vi.stubEnv('BUN_CONPTY_LIBRARY', join(root, 'conpty.dll')) + return root +} +describe('Windows relay provider admission', () => { + it.each(['x64', 'arm64'])( + 'admits the adjacent qualified %s pair without changing the selector', + (architecture) => { + fixture(architecture) + Object.defineProperty(process, 'arch', { value: architecture }) + const initial = process.env.BUN_CONPTY_LIBRARY + expect(() => assertWindowsRelayConptyProvider(daemon)).not.toThrow() + expect(process.env.BUN_CONPTY_LIBRARY).toBe(initial) + } + ) + it.each([undefined, '', 'relative/conpty.dll'])( + 'rejects absent or relative selector %s', + (selector) => { + fixture() + vi.stubEnv('BUN_CONPTY_LIBRARY', selector) + expect(() => assertWindowsRelayConptyProvider(daemon)).toThrow('before starting Bun') + } + ) + it('rejects a qualified provider from another installation', () => { + const foreign = fixture() + fixture() + vi.stubEnv('BUN_CONPTY_LIBRARY', join(foreign, 'conpty.dll')) + expect(() => assertWindowsRelayConptyProvider(daemon)).toThrow('this relay installation') + }) + it.each(['conpty.dll', 'OpenConsole.exe'])('rejects corrupt and missing %s', (file) => { + const root = fixture() + writeFileSync(join(root, file), 'corrupt') + expect(() => assertWindowsRelayConptyProvider(daemon)).toThrow('identity mismatch') + rmSync(join(root, file)) + expect(() => assertWindowsRelayConptyProvider(daemon)).toThrow() + }) + it('rejects the wrong architecture', () => { + fixture('arm64') + expect(() => assertWindowsRelayConptyProvider(daemon)).toThrow('identity mismatch') + }) + it('does not gate reconnect, CLI bridging, or POSIX on a Windows provider', () => { + vi.stubEnv('BUN_CONPTY_LIBRARY', undefined) + expect(() => assertWindowsRelayConptyProvider({ ...daemon, connectMode: true })).not.toThrow() + expect(() => assertWindowsRelayConptyProvider({ ...daemon, cliMode: true })).not.toThrow() + Object.defineProperty(process, 'platform', { value: 'linux' }) + expect(() => assertWindowsRelayConptyProvider(daemon)).not.toThrow() + }) +}) diff --git a/src/relay/windows-relay-conpty-admission.ts b/src/relay/windows-relay-conpty-admission.ts new file mode 100644 index 000000000000..67e8ef077f37 --- /dev/null +++ b/src/relay/windows-relay-conpty-admission.ts @@ -0,0 +1,29 @@ +import { realpathSync } from 'node:fs' +import { dirname, isAbsolute } from 'node:path' +import { resolveWindowsConptyProvider } from '../main/windows/windows-conpty-provider' +import { normalizeRuntimePathForComparison } from '../shared/cross-platform-path' +import type { RelayLaunchOptions } from './relay-launch-options' + +/** The native provider reads the initial environment; changing process.env here is too late. */ +export function assertWindowsRelayConptyProvider( + options: Pick +): void { + if (process.platform !== 'win32' || options.connectMode || options.cliMode) { + return + } + const entry = process.argv[1] + const selector = process.env.BUN_CONPTY_LIBRARY + if (!entry || !selector || !isAbsolute(selector)) { + throw new Error( + 'Windows relay requires its bundled terminal provider. Launch through Orca SSH deployment, ' + + 'or set BUN_CONPTY_LIBRARY to the absolute adjacent conpty.dll path before starting Bun.' + ) + } + const expected = resolveWindowsConptyProvider(dirname(realpathSync(entry)), process.arch) + if ( + normalizeRuntimePathForComparison(realpathSync(selector)) !== + normalizeRuntimePathForComparison(realpathSync(expected)) + ) { + throw new Error('Windows relay terminal provider must belong to this relay installation') + } +} diff --git a/src/shared/relay-artifacts.ts b/src/shared/relay-artifacts.ts index 28727ca8f30d..9172b6f9d906 100644 --- a/src/shared/relay-artifacts.ts +++ b/src/shared/relay-artifacts.ts @@ -50,6 +50,13 @@ export const RELAY_WINDOWS_PROCESS_TREE_FILENAME = 'windows-process-tree.node' export const RELAY_WINDOWS_WATCHER_FILENAME = 'parcel-watcher.node' export const RELAY_WINDOWS_WATCHER_LICENSE = 'parcel-watcher-LICENSE' export const RELAY_OPENCODE_SQLITE_READER_FILENAME = 'opencode-sqlite-reader.cjs' +export const RELAY_WINDOWS_CONPTY_FILENAMES = [ + 'conpty.dll', + 'OpenConsole.exe', + 'conpty-LICENSE.txt', + 'conpty.json', + 'Microsoft.Windows.Console.ConPTY.nuspec' +] as const export const RELAY_ARTIFACTS: readonly RelayArtifact[] = [ { filename: 'relay.js' }, @@ -57,6 +64,7 @@ export const RELAY_ARTIFACTS: readonly RelayArtifact[] = [ { filename: 'relay-watcher.js', daemonServiceChild: true }, { filename: RELAY_WINDOWS_WATCHER_FILENAME, windowsOnly: true }, { filename: RELAY_WINDOWS_WATCHER_LICENSE, windowsOnly: true }, + ...RELAY_WINDOWS_CONPTY_FILENAMES.map((filename) => ({ filename, windowsOnly: true })), { filename: 'relay-ai-vault-service.js', daemonServiceChild: true }, { filename: RELAY_OPENCODE_SQLITE_READER_FILENAME }, { filename: 'managed-hook-runtime.js' }, diff --git a/src/shared/relay-optional-artifacts.test.ts b/src/shared/relay-optional-artifacts.test.ts index 6b41d0320481..a47967eb27df 100644 --- a/src/shared/relay-optional-artifacts.test.ts +++ b/src/shared/relay-optional-artifacts.test.ts @@ -1,11 +1,39 @@ import { describe, expect, it } from 'vitest' import { RELAY_WINDOWS_PROCESS_TREE_FILENAME, + RELAY_WINDOWS_CONPTY_FILENAMES, relayArtifactFilenames, relayOptionalArtifactFilenames } from './relay-artifacts' describe('optional relay artifacts', () => { + it('requires the complete Windows provider without metadata name collisions', () => { + const windows = relayArtifactFilenames(true) + expect(RELAY_WINDOWS_CONPTY_FILENAMES).toEqual([ + 'conpty.dll', + 'OpenConsole.exe', + 'conpty-LICENSE.txt', + 'conpty.json', + 'Microsoft.Windows.Console.ConPTY.nuspec' + ]) + for (const filename of RELAY_WINDOWS_CONPTY_FILENAMES) { + expect(windows).toContain(filename) + expect(relayOptionalArtifactFilenames(true)).not.toContain(filename) + } + expect(new Set(windows).size).toBe(windows.length) + }) + + it('preserves the POSIX required artifact order and content', () => { + expect(relayArtifactFilenames(false)).toEqual([ + 'relay.js', + 'relay-watcher.js', + 'relay-ai-vault-service.js', + 'opencode-sqlite-reader.cjs', + 'managed-hook-runtime.js', + 'wsl-transcript-fs-process-entry.js' + ]) + }) + it('keeps the process-table addon out of the required set', () => { // The remote install probe requires every name this returns. Demanding an // artifact only a Windows build machine can emit would make a correct relay From 59ee100a0e835c34ac6ec53c8059e7df79b6cca8 Mon Sep 17 00:00:00 2001 From: m4air Date: Mon, 28 Sep 2026 17:51:09 -0700 Subject: [PATCH 2/3] fix(relay): verify console provider in release harness and signed package --- .github/workflows/release-cut.yml | 7 +++ config/scripts/build-windows-conpty.test.mjs | 62 +++++++++++++++++++ .../relay-daemon-launch-environment.mjs | 16 +++++ .../scripts/relay-watcher-fault-harness.mjs | 5 +- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 config/scripts/relay-daemon-launch-environment.mjs diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index e080112ad3f9..068037771280 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -1782,6 +1782,13 @@ jobs: node config/scripts/finalize-signed-cli-runtime.mjs $root 'signing-stage' if ($LASTEXITCODE -ne 0) { throw 'Signed runtime identity finalization failed.' } + - name: Verify relay provider identity after signing + if: matrix.platform == 'win' && github.run_attempt == 1 + shell: pwsh + run: | + node --input-type=module -e "import { verifyConptyDirectory } from './config/scripts/build-windows-conpty.mjs'; for (const arch of ['x64', 'arm64']) verifyConptyDirectory('dist/win-unpacked/resources/relay/win32-' + arch, arch)" + if ($LASTEXITCODE -ne 0) { throw 'Signing changed a pinned relay terminal provider.' } + # Why gated separately from the inner restore above: if SignPath's # windows-inner-binaries-zip artifact configuration does not (yet) cover the # uninstaller/ directory, the uninstaller comes back missing. That must cost diff --git a/config/scripts/build-windows-conpty.test.mjs b/config/scripts/build-windows-conpty.test.mjs index ebc3331a253c..7daa165a41a3 100644 --- a/config/scripts/build-windows-conpty.test.mjs +++ b/config/scripts/build-windows-conpty.test.mjs @@ -4,6 +4,7 @@ import { mkdirSync, mkdtempSync, readdirSync, + realpathSync, readFileSync, rmSync, writeFileSync @@ -32,6 +33,7 @@ vi.mock('./zip-extractor-command.mjs', () => ({ vi.mock('./script-child-process.mjs', () => ({ runProcessSync: vi.fn() })) import { runProcessSync } from './script-child-process.mjs' import { stageWindowsRelayConpty } from './relay-conpty-packaging.mjs' +import { relayDaemonLaunchEnvironment } from './relay-daemon-launch-environment.mjs' import { RELAY_WINDOWS_CONPTY_FILENAMES } from '../../src/shared/relay-artifacts.ts' import { downloadConptyArchive, @@ -205,3 +207,63 @@ describe('Windows relay provider packaging', () => { } ) }) + +describe('relay fault-harness provider environment', () => { + it.each(['x64', 'arm64'])('selects verified adjacent %s binaries before launch', async (arch) => { + mkdirSync(output) + await stageWindowsRelayConpty(`win32-${arch}`, output, { cacheDir, fetcher }) + const entry = join(output, 'relay.js') + writeFileSync(entry, '') + const env = { BUN_CONPTY_LIBRARY: 'foreign.dll', ORCA_WATCHER_CHILD_PID_FILE: 'watcher.pid' } + expect(relayDaemonLaunchEnvironment(entry, { platform: 'win32', arch, env })).toEqual({ + ...env, + BUN_CONPTY_LIBRARY: join(realpathSync(output), 'conpty.dll') + }) + expect(env.BUN_CONPTY_LIBRARY).toBe('foreign.dll') + writeFileSync(join(output, 'OpenConsole.exe'), 'modified by signing') + expect(() => relayDaemonLaunchEnvironment(entry, { platform: 'win32', arch, env })).toThrow( + 'checksum mismatch' + ) + rmSync(join(output, 'OpenConsole.exe')) + expect(() => relayDaemonLaunchEnvironment(entry, { platform: 'win32', arch, env })).toThrow() + }) + + it('leaves POSIX launches independent of Windows artifacts', () => { + const env = { ORCA_WATCHER_CHILD_PID_FILE: 'watcher.pid' } + expect(relayDaemonLaunchEnvironment('/missing/relay.js', { platform: 'linux', env })).toEqual( + env + ) + }) + + it('wires verified environment into the fault harness daemon spawn', () => { + const source = readFileSync( + new URL('./relay-watcher-fault-harness.mjs', import.meta.url), + 'utf8' + ) + expect(source).toContain( + "import { relayDaemonLaunchEnvironment } from './relay-daemon-launch-environment.mjs'" + ) + expect(source).toMatch( + /env: relayDaemonLaunchEnvironment\(relayEntry, \{\s*env: \{ \.\.\.process.env, ORCA_WATCHER_CHILD_PID_FILE: pidFile \}/ + ) + }) + + it('gates both relay providers after signing and before installer rebuild', () => { + const source = readFileSync( + new URL('../../.github/workflows/release-cut.yml', import.meta.url), + 'utf8' + ) + const start = source.indexOf(' - name: Verify relay provider identity after signing') + expect(start).toBeGreaterThan( + source.indexOf(' - name: Restore signed inner binaries into unpacked app') + ) + expect(start).toBeLessThan(source.indexOf(' id: rebuild-nsis-signed')) + const step = source.slice(start, source.indexOf(' - name:', start + 1)) + expect(step).toContain( + "for (const arch of ['x64', 'arm64']) verifyConptyDirectory('dist/win-unpacked/resources/relay/win32-' + arch, arch)" + ) + expect(step).toContain('if ($LASTEXITCODE -ne 0) { throw') + expect(step).not.toContain('continue-on-error') + expect(step).not.toContain('restore-signed-inner.outcome') + }) +}) diff --git a/config/scripts/relay-daemon-launch-environment.mjs b/config/scripts/relay-daemon-launch-environment.mjs new file mode 100644 index 000000000000..2113f7d2f29e --- /dev/null +++ b/config/scripts/relay-daemon-launch-environment.mjs @@ -0,0 +1,16 @@ +import { dirname, join } from 'node:path' +import { realpathSync } from 'node:fs' +import { verifyConptyDirectory } from './build-windows-conpty.mjs' + +/** Supply the native selector before Bun starts, replacing inherited foreign providers. */ +export function relayDaemonLaunchEnvironment(relayEntry, options = {}) { + const platform = options.platform ?? process.platform + const arch = options.arch ?? process.arch + const env = { ...(options.env ?? process.env) } + if (platform === 'win32') { + const directory = dirname(realpathSync(relayEntry)) + verifyConptyDirectory(directory, arch) + env.BUN_CONPTY_LIBRARY = join(directory, 'conpty.dll') + } + return env +} diff --git a/config/scripts/relay-watcher-fault-harness.mjs b/config/scripts/relay-watcher-fault-harness.mjs index 784abfc48501..01021aa6d97b 100644 --- a/config/scripts/relay-watcher-fault-harness.mjs +++ b/config/scripts/relay-watcher-fault-harness.mjs @@ -1,4 +1,5 @@ import { build } from 'esbuild' +import { relayDaemonLaunchEnvironment } from './relay-daemon-launch-environment.mjs' import { spawn } from 'node:child_process' import { randomBytes } from 'node:crypto' import { createRequire } from 'node:module' @@ -275,7 +276,9 @@ async function main() { ], { cwd: dirname(relayEntry), - env: { ...process.env, ORCA_WATCHER_CHILD_PID_FILE: pidFile }, + env: relayDaemonLaunchEnvironment(relayEntry, { + env: { ...process.env, ORCA_WATCHER_CHILD_PID_FILE: pidFile } + }), stdio: ['ignore', 'pipe', 'pipe'] } ) From 2084c58ba5410106ce61153a9fb16cdb4b6e5301 Mon Sep 17 00:00:00 2001 From: m4air Date: Mon, 28 Sep 2026 18:02:51 -0700 Subject: [PATCH 3/3] fix(relay): run watcher fault gate with bundled Bun --- config/scripts/build-windows-conpty.test.mjs | 17 +++++ .../relay-daemon-launch-environment.mjs | 5 ++ .../scripts/relay-fault-harness-runtime.mjs | 24 +++++++ .../relay-fault-harness-runtime.test.mjs | 71 +++++++++++++++++++ .../scripts/relay-watcher-fault-harness.mjs | 12 ++-- 5 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 config/scripts/relay-fault-harness-runtime.mjs create mode 100644 config/scripts/relay-fault-harness-runtime.test.mjs diff --git a/config/scripts/build-windows-conpty.test.mjs b/config/scripts/build-windows-conpty.test.mjs index 7daa165a41a3..14cd92db7727 100644 --- a/config/scripts/build-windows-conpty.test.mjs +++ b/config/scripts/build-windows-conpty.test.mjs @@ -267,3 +267,20 @@ describe('relay fault-harness provider environment', () => { expect(step).not.toContain('restore-signed-inner.outcome') }) }) + +it('removes runtime injection flags without mutating controller environment', () => { + const env = { + NODE_OPTIONS: '--require=foreign', + NODE_PATH: '/foreign', + BUN_OPTIONS: 'foreign', + BUN_INSPECT: '1', + ELECTRON_RUN_AS_NODE: '1', + PATH: '/keep', + ORCA_BACKGROUND_LAUNCH: '1' + } + expect(relayDaemonLaunchEnvironment('/missing/relay.js', { platform: 'linux', env })).toEqual({ + PATH: '/keep', + ORCA_BACKGROUND_LAUNCH: '1' + }) + expect(env.NODE_OPTIONS).toBe('--require=foreign') +}) diff --git a/config/scripts/relay-daemon-launch-environment.mjs b/config/scripts/relay-daemon-launch-environment.mjs index 2113f7d2f29e..fc197ab307ba 100644 --- a/config/scripts/relay-daemon-launch-environment.mjs +++ b/config/scripts/relay-daemon-launch-environment.mjs @@ -7,6 +7,11 @@ export function relayDaemonLaunchEnvironment(relayEntry, options = {}) { const platform = options.platform ?? process.platform const arch = options.arch ?? process.arch const env = { ...(options.env ?? process.env) } + for (const key of Object.keys(env)) { + if (/^(NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_INSPECT.*|ELECTRON_RUN_AS_NODE)$/i.test(key)) { + delete env[key] + } + } if (platform === 'win32') { const directory = dirname(realpathSync(relayEntry)) verifyConptyDirectory(directory, arch) diff --git a/config/scripts/relay-fault-harness-runtime.mjs b/config/scripts/relay-fault-harness-runtime.mjs new file mode 100644 index 000000000000..ef14e702f4c4 --- /dev/null +++ b/config/scripts/relay-fault-harness-runtime.mjs @@ -0,0 +1,24 @@ +import { join, resolve } from 'node:path' +import { cliRuntimeFilename, verifyCliRuntimeDirectory } from '../bundled-cli-runtime.cjs' +import { bunOwnedRuntimeArgs } from '../../src/shared/bun-owned-runtime-args.ts' + +/** The release build already prepares this runtime; the fault gate must never fetch one. */ +export function resolveRelayFaultHarnessRuntime( + root, + platform = process.platform, + arch = process.arch +) { + const directory = resolve(root, 'out', 'cli-runtime', `${platform}-${arch}`) + try { + verifyCliRuntimeDirectory(directory, platform, arch) + } catch (cause) { + throw new Error( + `Prepare the bundled CLI runtime for ${platform}-${arch} before the relay fault gate`, + { cause } + ) + } + return { + executable: join(directory, cliRuntimeFilename(platform)), + args: bunOwnedRuntimeArgs(platform) + } +} diff --git a/config/scripts/relay-fault-harness-runtime.test.mjs b/config/scripts/relay-fault-harness-runtime.test.mjs new file mode 100644 index 000000000000..4a896e62ef98 --- /dev/null +++ b/config/scripts/relay-fault-harness-runtime.test.mjs @@ -0,0 +1,71 @@ +import { createHash } from 'node:crypto' +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, expect, it } from 'vitest' +import { ORCAD_BUN_VERSION } from '../../src/shared/orcad-bun-runtime.ts' +import { resolveRelayFaultHarnessRuntime } from './relay-fault-harness-runtime.mjs' + +const roots = [] +afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }))) +function fixture(platform, arch) { + const root = mkdtempSync(join(tmpdir(), 'relay-fault-runtime-')) + roots.push(root) + const directory = join(root, 'out/cli-runtime', `${platform}-${arch}`) + mkdirSync(directory, { recursive: true }) + copyFileSync( + new URL('../../resources/licenses/bun/LICENSE.md', import.meta.url), + join(directory, 'LICENSE.md') + ) + const bytes = Buffer.from('fixture binary') + writeFileSync(join(directory, 'bun-runtime'), bytes) + writeFileSync( + join(directory, 'runtime.json'), + JSON.stringify({ + version: ORCAD_BUN_VERSION, + target: `${platform}-${arch}${platform === 'linux' ? '-glibc' : ''}`, + sha256: createHash('sha256').update(bytes).digest('hex') + }) + ) + return { root, directory } +} +it.each([ + ['darwin', 'arm64'], + ['darwin', 'x64'], + ['linux', 'x64'], + ['linux', 'arm64'] +])( + 'selects the existing verified %s/%s artifact, never the controller runtime', + (platform, arch) => { + const { root, directory } = fixture(platform, arch) + const runtime = resolveRelayFaultHarnessRuntime(root, platform, arch) + expect(runtime).toEqual({ + executable: join(directory, 'bun-runtime'), + args: ['--no-env-file', '--config=/dev/null', '--no-install'] + }) + expect(runtime.executable).not.toBe(process.execPath) + writeFileSync(join(directory, 'bun-runtime'), 'corrupt') + expect(() => resolveRelayFaultHarnessRuntime(root, platform, arch)).toThrow( + 'Prepare the bundled CLI runtime' + ) + } +) +it('rejects absent or wrong-architecture artifacts without a Node fallback', () => { + const { root } = fixture('darwin', 'arm64') + for (const [platform, arch] of [ + ['darwin', 'x64'], + ['win32', 'x64'], + ['win32', 'arm64'] + ]) { + expect(() => resolveRelayFaultHarnessRuntime(root, platform, arch)).toThrow( + 'Prepare the bundled CLI runtime' + ) + } +}) +it('uses the selected executable and owned arguments for both daemon and connector', () => { + const source = readFileSync(new URL('./relay-watcher-fault-harness.mjs', import.meta.url), 'utf8') + expect(source).not.toContain('spawn(process.execPath') + expect(source).toContain('spawn(runtime.executable, [...runtime.args, entryPath, ...args]') + expect(source).toMatch(/daemon = spawn\(\s*runtime.executable,\s*\[\s*\.\.\.runtime.args,/) + expect(source).toMatch(/createRelayClient\(\s*runtime,\s*relayEntry,/) +}) diff --git a/config/scripts/relay-watcher-fault-harness.mjs b/config/scripts/relay-watcher-fault-harness.mjs index 01021aa6d97b..401e91c8af42 100644 --- a/config/scripts/relay-watcher-fault-harness.mjs +++ b/config/scripts/relay-watcher-fault-harness.mjs @@ -1,5 +1,6 @@ import { build } from 'esbuild' import { relayDaemonLaunchEnvironment } from './relay-daemon-launch-environment.mjs' +import { resolveRelayFaultHarnessRuntime } from './relay-fault-harness-runtime.mjs' import { spawn } from 'node:child_process' import { randomBytes } from 'node:crypto' import { createRequire } from 'node:module' @@ -121,8 +122,8 @@ function waitForStdoutSentinel(proc, protocol, stderr) { ) } -function createRelayClient(entryPath, args, env, protocol) { - const proc = spawn(process.execPath, [entryPath, ...args], { +function createRelayClient(runtime, entryPath, args, env, protocol) { + const proc = spawn(runtime.executable, [...runtime.args, entryPath, ...args], { cwd: dirname(entryPath), env, stdio: ['pipe', 'pipe', 'pipe'] @@ -235,6 +236,7 @@ function includesWatchPath(params, targetPath) { } async function main() { + const runtime = resolveRelayFaultHarnessRuntime(resolve(import.meta.dirname, '../..')) const platform = `${process.platform}-${process.arch}` const relayEntry = resolve('out', 'relay', platform, 'relay.js') const watcherEntry = resolve('out', 'relay', platform, 'relay-watcher.js') @@ -261,8 +263,9 @@ async function main() { // Why detached + --connect: the daemon primary stdio is unproved, so it cannot open a consumer // session; only an endpoint-credential socket client is admitted for pty.data after #12746. daemon = spawn( - process.execPath, + runtime.executable, [ + ...runtime.args, relayEntry, '--detached', '--grace-time', @@ -286,9 +289,10 @@ async function main() { await waitForStdoutSentinel(daemon, protocol, daemonStreams.stderr) relay = createRelayClient( + runtime, relayEntry, ['--connect', '--sock-path', socketPath, '--credential-file', credentialFile], - process.env, + relayDaemonLaunchEnvironment(relayEntry), protocol ) await relay.sentinelReceived