mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
fix(ssh): package and verify the Windows terminal provider before relay startup
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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" }`
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user