fix(ssh): package and verify the Windows terminal provider before relay startup

This commit is contained in:
m4air
2026-09-28 17:32:24 -07:00
parent 399647f574
commit ca4a05a5df
11 changed files with 266 additions and 2 deletions
+2
View File
@@ -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({
+61 -1
View File
@@ -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()
}
)
})
@@ -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))
+25
View File
@@ -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 })
}
}
@@ -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(
+2 -1
View File
@@ -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" }`
)
}
+2
View File
@@ -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<void> {
const options = parseRelayLaunchOptions(process.argv)
assertWindowsRelayConptyProvider(options)
if (options.connectMode) {
runRelayConnectChannel(options.sockPath, readRelayEndpointCredential(options.credentialFile))
return
@@ -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()
})
})
@@ -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<RelayLaunchOptions, 'connectMode' | 'cliMode'>
): 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')
}
}
+8
View File
@@ -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' },
@@ -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