fix(windows): replace the managed CLI launcher with a native one (#24094)

* docs(security): add the antivirus clearance path for future releases

Every AV false positive here has been handled one vendor and one shipped
version at a time. Document the programs that clear future releases instead --
signer and product enrollment rather than per-build sample submission -- and add
a script that reports an RC's current detection state by hash, so a verdict is
found before users meet it in an issue report.

Hash lookup only by default; --upload transmits the artifact and stays manual.

* fix(windows): replace the managed CLI launcher with a native one

resources\bin\orca.exe was a csc-compiled MSIL assembly: a small, freshly
compiled .NET image in a user-writable directory that mutates environment
variables and proxies a child process. That is the shape .NET dropper
heuristics are trained on, and every verdict against it named the family --
MSILHeracles from two vendors, Wacatac!ml from a third. Signing the file does
not change its shape, so signing never cleared it.

Rebuild it in Rust. Same resolution, same environment contract, same argv
passthrough that keeps newline-bearing orchestration bodies intact (#8374), and
the child still inherits our environment block rather than an explicit map, so
a block carrying both PATH and Path survives (#12046). The PE now carries
publisher, version, icon and an asInvoker manifest from build.rs.

Refs #23383

* ci(windows): install the Rust toolchain before building the CLI launcher

The hosted runners happen to ship cargo, but a real Windows dev box does not --
verified on our own Windows QA host, where cargo and rustc were both absent.
Relying on the image means a future image change fails deep inside
electron-builder's native hook instead of at an obvious step.
This commit is contained in:
Neil
2026-09-30 02:49:03 -07:00
committed by GitHub
parent cef66fbab8
commit d2dbe2c385
16 changed files with 844 additions and 220 deletions
+19 -1
View File
@@ -1159,9 +1159,27 @@ jobs:
- name: Cache Windows CLI launcher
uses: actions/cache@v5
with:
path: native/windows-cli-launcher/.build
# Why the cargo directories ride along: a hit on .build skips the build
# entirely, but a miss otherwise recompiles the resource crate from a
# cold registry.
path: |
native/windows-cli-launcher/.build
native/windows-cli-launcher/target
~/.cargo/registry
key: windows-cli-launcher-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('native/windows-cli-launcher/**', 'config/scripts/build-windows-cli-launcher.mjs', 'resources/build/icon.ico', 'package.json') }}
# Why an explicit step rather than trusting the runner image: the packaged
# CLI launcher is a native Rust binary, and a host without cargo fails deep
# inside electron-builder's native hook instead of here.
- name: Ensure the Rust toolchain for the Windows CLI launcher
shell: pwsh
run: |
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
rustup toolchain install stable --profile minimal
rustup default stable
}
cargo --version
- name: Build package inputs
run: pnpm run build:release:parallel
+14
View File
@@ -1395,6 +1395,20 @@ jobs:
node config/scripts/build-windows-process-tree-relay-addon.mjs --arch=x64
node config/scripts/build-windows-process-tree-relay-addon.mjs --arch=arm64
# Why an explicit step rather than trusting the runner image: the packaged
# CLI launcher is a native Rust binary, and a Windows host without cargo
# fails deep inside electron-builder's native hook instead of here. Real
# Windows dev machines do not have it by default either.
- name: Ensure the Rust toolchain for the Windows CLI launcher
if: matrix.platform == 'win'
shell: pwsh
run: |
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
rustup toolchain install stable --profile minimal
rustup default stable
}
cargo --version
# Why ORCA_POSTHOG_WRITE_KEY here: this is the only build that
# produces a published binary, so this is the only place the secret
# needs to be in scope. The key is a PostHog *project* API key, not
+2
View File
@@ -28,6 +28,7 @@ out/
/build/
release/
native/**/.build/
native/windows-cli-launcher/target/
# node-gyp output for the vendored Windows registry addon; generated per host and ABI.
native/windows-registry/build/
native/windows-registry/bin/
@@ -122,6 +123,7 @@ docs/**
!docs/reference/agent-session-search-contract.md
!docs/reference/agent-status-store.md
!docs/reference/antigravity-readiness-evidence.md
!docs/reference/antivirus-prerelease-clearance.md
!docs/reference/git-compatibility.md
!docs/reference/headless-linux-server.md
!docs/reference/ime-regression-checklist.md
+1 -1
View File
@@ -80,7 +80,7 @@ Orca targets macOS, Linux, and Windows. Keep all platform-dependent behavior beh
- **Windows process enumeration**: read the table through `src/main/windows/windows-process-table.ts`, never by forking `powershell.exe`. See [`docs/reference/windows-process-enumeration.md`](./docs/reference/windows-process-enumeration.md).
- **Windows MSYS/Git Bash panes**: their children break away from the per-PTY job unless it is created without `JOB_OBJECT_LIMIT_BREAKAWAY_OK`, and a `conpty.node` built before that fix passes every existing gate. Before changing the per-PTY job or debugging `windows-msys-job.win32.test.ts`, read [`docs/reference/windows-msys-job-breakaway.md`](./docs/reference/windows-msys-job-breakaway.md).
- **Windows daemon-host relocation**: the terminal daemon runs from a copy of the app runtime under `%LOCALAPPDATA%`, which is what survives an auto-update. Before touching that copy, its exe name, or the NSIS uninstall macro, read [`docs/reference/windows-daemon-host-relocation.md`](./docs/reference/windows-daemon-host-relocation.md).
- **Windows EDR signal**: don't add `-ExecutionPolicy Bypass`, `-EncodedCommand`, `cmd.exe /c` with escaped free text, per-operation interpreter spawning, or runtime `Add-Type` compilation without reading [`docs/reference/windows-edr-posture.md`](./docs/reference/windows-edr-posture.md) first — behavioural EDR scores each of those, and being signed does not clear them.
- **Windows EDR signal**: don't add `-ExecutionPolicy Bypass`, `-EncodedCommand`, `cmd.exe /c` with escaped free text, per-operation interpreter spawning, or runtime `Add-Type` compilation without reading [`docs/reference/windows-edr-posture.md`](./docs/reference/windows-edr-posture.md) first — behavioural EDR scores each of those, and being signed does not clear them. For file verdicts on the bytes we ship — antivirus false positives, and the vendor programs that clear a release before users meet the detection — see [`docs/reference/antivirus-prerelease-clearance.md`](./docs/reference/antivirus-prerelease-clearance.md).
- **WSL commands**: build argv with `buildWslExecArgs` (always `--exec` — under `--`, `wsl.exe` expands `$name` in every argument and silently rewrites the script), and fence anything whose stdout you parse with `buildWslCapturedLoginShellCommand`, because the interactive login shell prints the distro banner to stdout. See [`docs/reference/wsl-command-execution.md`](./docs/reference/wsl-command-execution.md).
- **Linux native modules**: keep the glibc floor at Ubuntu 20.04 / glibc 2.31. A module compiled from source on a newer runner can reference symbol versions absent on the floor and crash the app on startup. See [`docs/reference/linux-glibc-compatibility.md`](./docs/reference/linux-glibc-compatibility.md); packaging fails if a bundled native binary needs newer glibc.
+41 -46
View File
@@ -2,7 +2,7 @@
import { spawnSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -23,37 +23,22 @@ export function shouldReuseCompiledWindowsCliLauncher(outputPath, fingerprint) {
)
}
export function windowsCliLauncherVersionSource(version) {
const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(version)
/**
* The four-part numeric version Windows records in the PE. A prerelease suffix
* survives only in ProductVersion, which is a free-form string.
*/
export function windowsCliLauncherFileVersion(version) {
const match = /^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(version ?? '')
if (!match || match.slice(1).some((part) => Number(part) > 65534)) {
throw new Error(`Invalid Windows CLI launcher version: ${version}`)
}
const fileVersion = `${match.slice(1).join('.')}.0`
return [
'using System.Reflection;',
`[assembly: AssemblyVersion("${fileVersion}")]`,
`[assembly: AssemblyFileVersion("${fileVersion}")]`,
`[assembly: AssemblyInformationalVersion("${version}")]`,
''
].join('\n')
return `${match.slice(1).join('.')}.0`
}
function defaultOutputPath(projectRoot) {
return join(projectRoot, 'native', 'windows-cli-launcher', '.build', 'orca.exe')
}
function findFrameworkCompiler(env) {
const windowsDirectory = env.WINDIR ?? env.SystemRoot
if (!windowsDirectory) {
return null
}
const candidates = [
join(windowsDirectory, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe'),
join(windowsDirectory, 'Microsoft.NET', 'Framework', 'v4.0.30319', 'csc.exe')
]
return candidates.find((candidate) => existsSync(candidate)) ?? null
}
function readArg(name) {
const index = process.argv.indexOf(name)
return index !== -1 ? process.argv[index + 1] : undefined
@@ -69,59 +54,69 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
}
const repoRoot = resolve(import.meta.dirname, '../..')
const sourcePath = join(repoRoot, 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs')
const manifestPath = join(repoRoot, 'native', 'windows-cli-launcher', 'app.manifest')
const crateRoot = join(repoRoot, 'native', 'windows-cli-launcher')
const manifestPath = join(crateRoot, 'Cargo.toml')
const iconPath = join(repoRoot, 'resources', 'build', 'icon.ico')
const { version } = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'))
const versionSource = windowsCliLauncherVersionSource(version)
// Throws on a version the PE cannot represent, before anything is compiled.
windowsCliLauncherFileVersion(version)
const fingerprint = windowsCliLauncherFingerprint(
[
sourcePath,
join(crateRoot, 'src', 'main.rs'),
join(crateRoot, 'build.rs'),
manifestPath,
join(crateRoot, 'app.manifest'),
iconPath,
join(repoRoot, 'config/scripts/build-windows-cli-launcher.mjs')
],
version
)
const outputPath = readArg('--output') ?? defaultOutputPath(repoRoot)
const compilerPath = findFrameworkCompiler(process.env)
if (!compilerPath) {
throw new Error('Unable to find the .NET Framework C# compiler required for orca.exe.')
}
mkdirSync(dirname(outputPath), { recursive: true })
if (shouldReuseCompiledWindowsCliLauncher(outputPath, fingerprint)) {
console.log(`[native-build] reusing Windows CLI launcher at ${outputPath}`)
process.exit(0)
}
const versionPath = join(dirname(outputPath), 'OrcaCliLauncher.Version.cs')
writeFileSync(versionPath, versionSource)
rmSync(`${outputPath}.sha256`, { force: true })
const targetDirectory = join(crateRoot, 'target')
const result = spawnSync(
compilerPath,
'cargo',
[
'/nologo',
'/target:exe',
'/optimize+',
'/warnaserror+',
`/win32manifest:${manifestPath}`,
`/win32icon:${iconPath}`,
`/out:${outputPath}`,
sourcePath,
versionPath
'build',
'--release',
'--locked',
'--manifest-path',
manifestPath,
'--target-dir',
targetDirectory
],
{ cwd: repoRoot, stdio: 'inherit' }
{
cwd: crateRoot,
stdio: 'inherit',
env: {
...process.env,
ORCA_LAUNCHER_VERSION: version,
ORCA_LAUNCHER_ICON: iconPath
}
}
)
if (result.signal) {
process.kill(process.pid, result.signal)
}
if (result.error) {
throw result.error
// Why named explicitly: a bare ENOENT here reads as a missing source file
// rather than a host without the toolchain the packaged CLI needs.
throw new Error(
`Unable to run cargo for the Windows CLI launcher: ${result.error.message}. Install Rust (https://rustup.rs) and retry.`
)
}
if (result.status !== 0) {
process.exit(result.status ?? 1)
}
copyFileSync(join(targetDirectory, 'release', 'orca.exe'), outputPath)
writeFileSync(`${outputPath}.sha256`, fingerprint)
}
@@ -14,8 +14,8 @@ import { spawnSync } from 'node:child_process'
import { describe, expect, it } from 'vitest'
import {
shouldReuseCompiledWindowsCliLauncher,
windowsCliLauncherFingerprint,
windowsCliLauncherVersionSource
windowsCliLauncherFileVersion,
windowsCliLauncherFingerprint
} from './build-windows-cli-launcher.mjs'
const itCrossHost = process.platform === 'win32' ? it.skip : it
@@ -35,22 +35,25 @@ function removeFixtureTree(path) {
}
}
}
// Why: cold csc.exe startup exceeds Vitest's 5s unit budget on hosted Windows;
// keep the larger allowance scoped to the real compiler integration test.
// Why: a cold cargo build compiles the resource crate and links from scratch,
// which far exceeds Vitest's 5s unit budget on a hosted Windows runner. Later
// cases reuse the shared target directory, so only the first pays it.
function itWindows(name, test) {
const runner = process.platform === 'win32' ? it : it.skip
runner(name, { timeout: 15_000 }, test)
runner(name, { timeout: 300_000 }, test)
}
describe('Windows CLI launcher', () => {
it('reuses restored builds only while all embedded inputs and the release version match', () => {
const root = mkdtempSync(join(tmpdir(), 'orca-cli-launcher-reuse-'))
try {
const inputs = ['source.cs', 'app.manifest', 'icon.ico', 'build.mjs'].map((name) => {
const path = join(root, name)
writeFileSync(path, name)
return path
})
const inputs = ['main.rs', 'build.rs', 'Cargo.toml', 'app.manifest', 'icon.ico'].map(
(name) => {
const path = join(root, name)
writeFileSync(path, name)
return path
}
)
const outputPath = join(root, 'orca.exe')
const fingerprint = windowsCliLauncherFingerprint(inputs, '1.4.214')
expect(shouldReuseCompiledWindowsCliLauncher(outputPath, fingerprint)).toBe(false)
@@ -88,12 +91,11 @@ describe('Windows CLI launcher', () => {
}
})
it('keeps prerelease identity while emitting a valid Windows numeric version', () => {
const source = windowsCliLauncherVersionSource('1.4.214-daily.202609281300')
expect(source).toContain('AssemblyFileVersion("1.4.214.0")')
expect(source).toContain('AssemblyInformationalVersion("1.4.214-daily.202609281300")')
it('reduces a prerelease to the numeric version Windows can record', () => {
expect(windowsCliLauncherFileVersion('1.4.214-daily.202609281300')).toBe('1.4.214.0')
expect(windowsCliLauncherFileVersion('1.4.214')).toBe('1.4.214.0')
for (const version of ['1.4.65535', '1.4', '1.4.214"', undefined]) {
expect(() => windowsCliLauncherVersionSource(version)).toThrow('Invalid Windows')
expect(() => windowsCliLauncherFileVersion(version)).toThrow('Invalid Windows')
}
})
@@ -156,18 +158,28 @@ describe('Windows CLI launcher', () => {
}
})
itCrossHost('never materializes the child environment block from ProcessStartInfo', () => {
// Why: both ProcessStartInfo env properties copy the process block into a case-insensitive
// dictionary that throws when the inherited block holds PATH and Path (stablyai/orca#12046).
itCrossHost('never hands the child an explicit environment map', () => {
// Why: setting any entry on the child's environment makes the spawn build its own
// block from a case-insensitive map, which collapses an inherited PATH and Path
// into one entry and killed the CLI (stablyai/orca#12046). Mutating this process
// and leaving the map untouched passes the block through verbatim.
const source = readFileSync(
join(projectRoot, 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs'),
join(projectRoot, 'native', 'windows-cli-launcher', 'src', 'main.rs'),
'utf8'
)
const code = source.replace(/^\s*\/\/.*$/gm, '')
expect(code).not.toContain('EnvironmentVariables')
expect(code).not.toContain('startInfo.Environment')
expect(code).toContain('Environment.SetEnvironmentVariable')
expect(code).not.toMatch(/\.envs?\(/u)
expect(code).not.toContain('env_clear')
expect(code).toContain('env::set_var')
})
itCrossHost('never reintroduces a managed launcher alongside the native one', () => {
// Why: the MSIL image is what vendors flagged (stablyai/orca#23383). A stray .cs
// left in the crate would compile back into the shape the rewrite removed.
expect(
existsSync(join(projectRoot, 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs'))
).toBe(false)
})
itWindows('preserves a multiline argument from PowerShell through the native launcher', () => {
+1 -1
View File
@@ -194,7 +194,7 @@ describe('per-job path classification', () => {
})
it('runs native package jobs only for the platform that ships the changed native', () => {
expectClassification(['native/windows-cli-launcher/OrcaCliLauncher.cs'], {
expectClassification(['native/windows-cli-launcher/src/main.rs'], {
package_windows: true
})
expectClassification(['native/computer-use-linux/runtime.py'], {
@@ -0,0 +1,164 @@
#!/usr/bin/env node
/**
* Report the current antivirus detection state of built release artifacts, so an
* RC can be checked before users meet the verdict in an issue report. See
* docs/reference/antivirus-prerelease-clearance.md.
*
* Hash lookup only by default: nothing leaves the machine but a SHA-256. The
* `--upload` flag transmits the artifact itself, which distributes it to partner
* vendors — intended for clearance work, never for an automated path.
*/
import { createHash } from 'node:crypto'
import { createReadStream, openAsBlob } from 'node:fs'
import { stat } from 'node:fs/promises'
import { basename } from 'node:path'
import { pathToFileURL } from 'node:url'
const VIRUSTOTAL_FILES_ENDPOINT = 'https://www.virustotal.com/api/v3/files'
export async function hashFile(filePath) {
const hash = createHash('sha256')
for await (const chunk of createReadStream(filePath)) {
hash.update(chunk)
}
return hash.digest('hex')
}
/**
* The engines that call an artifact malicious or suspicious. Every other
* category (undetected, type-unsupported, timeout, failure) is silence, not a
* clean bill of health, so it is counted but never reported as a verdict.
*/
export function summarizeEngineVerdicts(analysisResults) {
const flagged = []
let scanned = 0
for (const [engine, result] of Object.entries(analysisResults ?? {})) {
scanned += 1
if (result?.category === 'malicious' || result?.category === 'suspicious') {
flagged.push({
engine,
category: result.category,
detection: result.result ?? '<unnamed>'
})
}
}
flagged.sort((left, right) => left.engine.localeCompare(right.engine))
return { scanned, flagged }
}
export function formatArtifactReport({ name, sha256, known, scanned, flagged }) {
if (!known) {
return `${name}\n sha256 ${sha256}\n no report yet — this build has never been scanned`
}
if (flagged.length === 0) {
return `${name}\n sha256 ${sha256}\n clean across ${scanned} engines`
}
const lines = flagged.map(
(entry) => ` ${entry.category.padEnd(10)} ${entry.engine}: ${entry.detection}`
)
return [
name,
` sha256 ${sha256}`,
` ${flagged.length} of ${scanned} engines flag this build:`,
...lines
].join('\n')
}
/** True when any artifact carries a verdict a user could hit. */
export function hasAnyDetection(reports) {
return reports.some((report) => report.flagged.length > 0)
}
async function fetchExistingReport(sha256, apiKey) {
const response = await fetch(`${VIRUSTOTAL_FILES_ENDPOINT}/${sha256}`, {
headers: { 'x-apikey': apiKey }
})
if (response.status === 404) {
return null
}
if (!response.ok) {
throw new Error(`VirusTotal lookup failed for ${sha256}: ${response.status}`)
}
const body = await response.json()
return body?.data?.attributes?.last_analysis_results ?? {}
}
// Why a separate upload URL above 32MB: the direct endpoint rejects larger
// files, and every Orca installer is far past that.
async function uploadArtifact(filePath, apiKey) {
const { size } = await stat(filePath)
let endpoint = VIRUSTOTAL_FILES_ENDPOINT
if (size > 32 * 1024 * 1024) {
const urlResponse = await fetch(`${VIRUSTOTAL_FILES_ENDPOINT}/upload_url`, {
headers: { 'x-apikey': apiKey }
})
if (!urlResponse.ok) {
throw new Error(`Could not obtain a VirusTotal upload URL: ${urlResponse.status}`)
}
endpoint = (await urlResponse.json())?.data
}
// openAsBlob streams from disk; buffering a 200MB installer would not fit.
const form = new FormData()
form.append('file', await openAsBlob(filePath), basename(filePath))
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'x-apikey': apiKey },
body: form
})
if (!response.ok) {
throw new Error(`VirusTotal upload failed for ${filePath}: ${response.status}`)
}
}
export async function scanArtifacts(filePaths, { apiKey, upload = false } = {}) {
const reports = []
for (const filePath of filePaths) {
const sha256 = await hashFile(filePath)
let analysisResults = await fetchExistingReport(sha256, apiKey)
if (analysisResults === null && upload) {
await uploadArtifact(filePath, apiKey)
analysisResults = null
}
const { scanned, flagged } = summarizeEngineVerdicts(analysisResults)
reports.push({
name: basename(filePath),
sha256,
known: analysisResults !== null,
scanned,
flagged
})
}
return reports
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const args = process.argv.slice(2)
const upload = args.includes('--upload')
const filePaths = args.filter((arg) => !arg.startsWith('--'))
if (filePaths.length === 0) {
throw new Error(
'Usage: node config/scripts/scan-release-artifacts-antivirus.mjs [--upload] <artifact>...'
)
}
const apiKey = process.env.VIRUSTOTAL_API_KEY
if (!apiKey) {
// Why not a failure: this runs alongside release steps that must not break
// before the secret is provisioned.
console.log('VIRUSTOTAL_API_KEY is not set; skipping the antivirus detection report.')
process.exit(0)
}
const reports = await scanArtifacts(filePaths, { apiKey, upload })
for (const report of reports) {
console.log(formatArtifactReport(report))
}
if (hasAnyDetection(reports)) {
// Why a warning and not an exit code: these are third-party ML classifiers,
// so a hard gate hands them the power to fail our releases. A human decides.
console.log(
'\nAt least one engine flags a shipped artifact. Decide before publishing, and submit ' +
'for clearance per docs/reference/antivirus-prerelease-clearance.md.'
)
}
}
@@ -0,0 +1,96 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
formatArtifactReport,
hasAnyDetection,
hashFile,
summarizeEngineVerdicts
} from './scan-release-artifacts-antivirus.mjs'
describe('antivirus detection report', () => {
it('hashes an artifact with the same digest the issue reports quote', async () => {
const directory = await mkdtemp(join(tmpdir(), 'orca-av-scan-'))
try {
const artifact = join(directory, 'orca-windows-setup.exe')
await writeFile(artifact, 'orca', 'utf8')
// Lowercase hex sha256, so a reporter's `shasum -a 256` output and ours
// compare directly — that comparison is what makes a submission credible.
expect(await hashFile(artifact)).toBe(
'e0c924608fdcda8536bd9cc86b0fce0ab2d54ecc1e8ed9673624c39cde7f7820'
)
} finally {
await rm(directory, { recursive: true, force: true })
}
})
it('counts only malicious and suspicious categories as verdicts', () => {
const { scanned, flagged } = summarizeEngineVerdicts({
Microsoft: { category: 'malicious', result: 'Trojan:Win32/Wacatac.B!ml' },
Bitdefender: { category: 'suspicious', result: 'Gen:Variant.MSILHeracles' },
Kaspersky: { category: 'undetected', result: null },
ESET: { category: 'type-unsupported', result: null },
Avast: { category: 'timeout', result: null }
})
expect(scanned).toBe(5)
expect(flagged).toEqual([
{ engine: 'Bitdefender', category: 'suspicious', detection: 'Gen:Variant.MSILHeracles' },
{ engine: 'Microsoft', category: 'malicious', detection: 'Trojan:Win32/Wacatac.B!ml' }
])
})
it('treats a missing results payload as nothing scanned rather than clean', () => {
expect(summarizeEngineVerdicts(undefined)).toEqual({ scanned: 0, flagged: [] })
expect(summarizeEngineVerdicts(null)).toEqual({ scanned: 0, flagged: [] })
})
it('names an unnamed detection instead of printing undefined', () => {
const { flagged } = summarizeEngineVerdicts({ Sophos: { category: 'malicious' } })
expect(flagged[0].detection).toBe('<unnamed>')
})
it('distinguishes an unscanned build from a clean one', () => {
const unscanned = formatArtifactReport({
name: 'orca-windows-setup.exe',
sha256: 'a'.repeat(64),
known: false,
scanned: 0,
flagged: []
})
expect(unscanned).toContain('never been scanned')
const clean = formatArtifactReport({
name: 'orca-windows-setup.exe',
sha256: 'a'.repeat(64),
known: true,
scanned: 70,
flagged: []
})
expect(clean).toContain('clean across 70 engines')
})
it('reports every flagging engine and its detection name', () => {
const report = formatArtifactReport({
name: 'orca.exe',
sha256: 'b'.repeat(64),
known: true,
scanned: 70,
flagged: [
{ engine: 'TrendMicro', category: 'malicious', detection: 'Trojan.MSIL.MSILHERACLES' }
]
})
expect(report).toContain('1 of 70 engines flag this build')
expect(report).toContain('TrendMicro: Trojan.MSIL.MSILHERACLES')
})
it('flags the release when any single artifact carries a verdict', () => {
const clean = { flagged: [] }
const flagged = { flagged: [{ engine: 'Microsoft' }] }
expect(hasAnyDetection([clean, clean])).toBe(false)
expect(hasAnyDetection([clean, flagged])).toBe(true)
})
})
@@ -0,0 +1,117 @@
# Antivirus clearance for future releases
Orca collects a steady stream of antivirus and EDR false positives — see the
tracking issue for the current grouping. This document covers the part of that
problem worth engineering effort: **stopping the next release from being
flagged.**
Clearing a *historic* release is explicitly not a goal. A user sitting on a
flagged build should update to a cleared one, not wait for a vendor to whitelist
a version we no longer ship. Retroactive submissions cost the same effort per
vendor and expire the moment we cut a new version.
For the behavioural side of the problem — the process-tree shapes EDR scores,
which no whitelist fixes — read
[`windows-edr-posture.md`](./windows-edr-posture.md) instead. This document is
about file verdicts on the bytes we ship.
## The two mechanisms, and only one of them scales
**Sample submission** clears one build. You send the flagged file to a vendor's
analyst portal, they confirm it is clean, and the verdict is dropped from their
next definition update. This is reactive and per-release: cutting a new version
produces new bytes, new hashes, and a fresh chance of the same heuristic firing.
Doing this every release, across every vendor, is not sustainable.
**Signer and product whitelisting** clears every future build. The vendor
records the publisher identity or enrolls the product in a dynamic allowlist, and
subsequent releases inherit that trust without another submission. Enrollment is
one-time work per vendor, and it is the only lever that scales with our release
cadence.
Prefer enrollment. Use submission only to clear a live incident while enrollment
is pending, and only for a release users are actually expected to install.
## Prerequisites that make enrollment possible
None of these programs will accept an unsigned or anonymous binary, so these
come first:
1. **Every shipped PE is Authenticode-signed, and CI fails the release if not.**
Done as of v1.4.217 — the release workflow requires a valid SignPath
Foundation signature on the inner binaries and no longer fails open.
2. **Every shipped PE carries real provenance** — company, product, version,
description, and an explicit `asInvoker` manifest. An anonymous binary scores
worse than an identified one, and several portals reject submissions that
carry no version metadata.
3. **One stable signer identity.** Vendor allowlists key on the certificate
subject. Rotating signers resets accrued reputation, so a certificate change
is a re-enrollment event, not a transparent swap.
## Vendor programs
Enrollment state is deliberately left as a task here rather than asserted — fill
each in as it is confirmed, and record the account that owns it so a lapsed
enrollment is traceable.
| Vendor | Mechanism | Scope | State |
| ------------------------- | ---------------------------------------------------------------------------- | ------------------------ | ----- |
| **VirusTotal** | Monitor — paid; builds rescanned daily, developer and vendor both notified | ~70 engines at once | TODO |
| **Microsoft** | Defender Security Intelligence submission, as a software developer | Defender, Defender FP EP | TODO |
| **Microsoft** | Trusted Signing, or an EV certificate, for SmartScreen and Smart App Control | Reputation gates | TODO |
| **Kaspersky** | Whitelist Program — vendors submit builds for the Dynamic Allowlist | Endpoint, all platforms | TODO |
| **Trend Micro** | Certified Safe Software Service — pre-release software whitelisting | Endpoint, Virus Buster | TODO |
| **Bitdefender** | False-positive submission for software vendors | Endpoint, ATD | TODO |
| **Avast / AVG / Norton** | Gen Digital false-positive and whitelisting channels | Consumer suites | TODO |
| **ESET** | False-positive sample submission | Endpoint | TODO |
| **Tencent iOA** | No public developer channel found; needs a support relationship | iOA, macOS and Windows | TODO |
VirusTotal Monitor is the highest-leverage single entry, because it is the only
channel built for exactly this workflow: uploads sit in a private store, get
rescanned daily against every engine's current signatures, and when one flags a
file **both we and that vendor are notified automatically**. Pre-publish upload
is a supported use, which is precisely the future-release posture we want. It is
a paid service, monetised on developers and free to the antivirus vendors.
Be honest about its limit: VirusTotal states plainly that Monitor is not a free
pass to get a file whitelisted. Vendors sometimes keep a detection. What it
reliably buys is *early notice and a real contact path* instead of discovering a
verdict from a user's issue report weeks later.
Do not treat a plain VirusTotal *scan* as equivalent. A scan tells us a verdict
exists; Monitor is what routes it to someone who can drop it.
If the subscription is not worth it, the free fallback is the community-maintained
false-positive contact directory (`yaronelh/False-Positive-Center` on GitHub),
which collects the submission addresses and forms each vendor actually reads.
That replaces the hardest part of a submission — finding the right contact — but
keeps the per-release effort that Monitor removes.
## Where this lands in the release flow
The check belongs at RC time, not after a stable cut — a verdict discovered after
publication is a verdict users already hit.
`config/scripts/scan-release-artifacts-antivirus.mjs` reports the current
detection state of built artifacts by hash. Run it against an RC's artifacts, and
treat any engine verdict as a release-blocking question rather than an automatic
stop: these are third-party ML classifiers, so a hard gate on their output would
fail the release for reasons outside our control. Read the report, decide, and
submit if the flagged build is one we intend to ship.
The script looks up hashes by default and never transmits artifact bytes. Passing
`--upload` sends the file to VirusTotal, which distributes samples to partner
vendors — that is the intended outcome for clearance work, but it is a
publication, so it stays opt-in and out of any automated path.
## What not to do
- **Do not ask users to add exclusions** as the resolution. It suppresses the
symptom on one machine, and in several reports here the exclusion did not even
hold because the detection was behavioural rather than path-based.
- **Do not dispute a verdict without a sample.** Every report worth acting on in
this project came with a hash that we verified bit-identical to the published
release asset. That verification is what makes a submission credible.
- **Do not chase a vendor whose detection we cannot reproduce or name.** Route
those back to the reporter for the detection string and the exact flagged path
first.
+157
View File
@@ -0,0 +1,157 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "indexmap"
version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "orca-cli-launcher"
version = "0.0.0"
dependencies = [
"winresource",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "syn"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "winnow"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
[[package]]
name = "winresource"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087"
dependencies = [
"toml",
"version_check",
]
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "orca-cli-launcher"
version = "0.0.0"
edition = "2021"
publish = false
# Why the bin is named `orca`: it ships as resources\bin\orca.exe, and the PE's
# OriginalFilename must match the file users actually invoke.
[[bin]]
name = "orca"
path = "src/main.rs"
[build-dependencies]
winresource = "0.1"
# Why: this is a launcher, not a program. Everything here trims the PE toward the
# shape of an ordinary small native utility -- no panic machinery, no debug
# sections, one codegen unit -- which is also what keeps it out of the
# heuristic bucket the previous managed build sat in.
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true
@@ -1,137 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("Orca CLI Launcher")]
[assembly: AssemblyDescription("Command-line launcher for Orca")]
[assembly: AssemblyCompany("Stably AI")]
[assembly: AssemblyProduct("Orca")]
[assembly: AssemblyCopyright("Copyright © Stably AI")]
[assembly: ComVisible(false)]
internal static class OrcaCliLauncher
{
private static int Main(string[] args)
{
try
{
string launcherDirectory = Path.GetDirectoryName(typeof(OrcaCliLauncher).Assembly.Location);
string resourcesDirectory = Directory.GetParent(launcherDirectory).FullName;
string appDirectory = Directory.GetParent(resourcesDirectory).FullName;
string electronPath = Path.Combine(appDirectory, "Orca.exe");
string cliPath = Path.Combine(
resourcesDirectory,
"app.asar.unpacked",
"out",
"cli",
"index.js"
);
if (!File.Exists(electronPath))
{
Console.Error.WriteLine("Unable to locate Orca.exe next to \"{0}\"", resourcesDirectory);
return 1;
}
if (!File.Exists(cliPath))
{
Console.Error.WriteLine("Unable to locate the Orca CLI entrypoint at \"{0}\"", cliPath);
return 1;
}
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = electronPath,
Arguments = BuildArguments(cliPath, args),
UseShellExecute = false
};
// Why: launching without cmd.exe preserves embedded newlines while matching the
// packaged batch launcher's Electron-as-Node environment contract.
// Why: ProcessStartInfo's env copy rejects duplicate PATH/Path keys; mutating this
// short-lived process preserves the native block for child inheritance (#12046).
MoveEnvironmentVariable("NODE_OPTIONS", "ORCA_NODE_OPTIONS");
MoveEnvironmentVariable("NODE_REPL_EXTERNAL_MODULE", "ORCA_NODE_REPL_EXTERNAL_MODULE");
Environment.SetEnvironmentVariable("ELECTRON_RUN_AS_NODE", "1");
Environment.SetEnvironmentVariable("ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER", "1");
string requestedCliCommand = Environment.GetEnvironmentVariable("ORCA_CLI_COMMAND");
Environment.SetEnvironmentVariable(
"ORCA_CLI_COMMAND",
requestedCliCommand == "orca-ide" ? "orca-ide" : "orca"
);
using (Process child = Process.Start(startInfo))
{
child.WaitForExit();
return child.ExitCode;
}
}
catch (Exception error)
{
Console.Error.WriteLine("Unable to start the Orca CLI: {0}", error.Message);
return 1;
}
}
private static void MoveEnvironmentVariable(string sourceName, string targetName)
{
string value = Environment.GetEnvironmentVariable(sourceName);
Environment.SetEnvironmentVariable(sourceName, null);
// Why: a null value clears the target, matching the previous unconditional Remove.
Environment.SetEnvironmentVariable(targetName, value);
}
private static string BuildArguments(string cliPath, string[] args)
{
StringBuilder commandLine = new StringBuilder(QuoteArgument(cliPath));
foreach (string arg in args)
{
commandLine.Append(' ');
commandLine.Append(QuoteArgument(arg));
}
return commandLine.ToString();
}
private static string QuoteArgument(string value)
{
bool requiresQuotes = value.Length == 0;
for (int index = 0; index < value.Length && !requiresQuotes; index += 1)
{
requiresQuotes = value[index] == '"' || Char.IsWhiteSpace(value[index]);
}
if (!requiresQuotes)
{
return value;
}
StringBuilder quoted = new StringBuilder("\"");
int backslashCount = 0;
foreach (char character in value)
{
if (character == '\\')
{
backslashCount += 1;
continue;
}
if (character == '"')
{
quoted.Append('\\', backslashCount * 2 + 1);
quoted.Append('"');
}
else
{
quoted.Append('\\', backslashCount);
quoted.Append(character);
}
backslashCount = 0;
}
quoted.Append('\\', backslashCount * 2);
quoted.Append('"');
return quoted.ToString();
}
}
+63
View File
@@ -0,0 +1,63 @@
//! Embeds the Windows PE version block, application manifest, and icon.
//!
//! Why this matters beyond cosmetics: an anonymous binary with no publisher,
//! no version, and no declared execution level scores worse under antivirus
//! heuristics than an identified one, and several vendor submission portals
//! reject a sample that carries no version metadata at all.
use std::env;
fn main() {
println!("cargo:rerun-if-changed=app.manifest");
println!("cargo:rerun-if-env-changed=ORCA_LAUNCHER_VERSION");
println!("cargo:rerun-if-env-changed=ORCA_LAUNCHER_ICON");
if env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") {
return;
}
// Set by config/scripts/build-windows-cli-launcher.mjs from package.json, so
// the launcher always reports the release it shipped in.
let version = env::var("ORCA_LAUNCHER_VERSION")
.expect("ORCA_LAUNCHER_VERSION must be set; build through build-windows-cli-launcher.mjs");
let (major, minor, patch) = numeric_version_parts(&version);
let mut resource = winresource::WindowsResource::new();
resource.set("ProductName", "Orca");
resource.set("FileDescription", "Orca CLI Launcher");
resource.set("CompanyName", "Stably AI");
resource.set(
"LegalCopyright",
"Copyright (C) Stably AI. All rights reserved.",
);
resource.set("InternalName", "orca.exe");
resource.set("OriginalFilename", "orca.exe");
resource.set("FileVersion", &format!("{major}.{minor}.{patch}.0"));
resource.set("ProductVersion", &version);
resource.set_version_info(
winresource::VersionInfo::FILEVERSION,
(major << 48) | (minor << 32) | (patch << 16),
);
resource.set_version_info(
winresource::VersionInfo::PRODUCTVERSION,
(major << 48) | (minor << 32) | (patch << 16),
);
resource.set_manifest_file("app.manifest");
if let Ok(icon) = env::var("ORCA_LAUNCHER_ICON") {
resource.set_icon(&icon);
}
resource
.compile()
.expect("failed to compile Windows resources");
}
/// Strips any prerelease or build suffix; the PE numeric version accepts digits only.
fn numeric_version_parts(version: &str) -> (u64, u64, u64) {
let base = version.split(['-', '+']).next().unwrap_or_default();
let mut parts = base.split('.').map(|part| part.parse::<u64>().unwrap_or(0));
(
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
parts.next().unwrap_or(0),
)
}
+102
View File
@@ -0,0 +1,102 @@
//! Launches the Orca CLI by running the packaged Electron binary as Node.
//!
//! Why a native binary rather than the `.cmd` alone: `cmd.exe` reparses `%*` and
//! executes or truncates embedded newlines, so orchestration message bodies
//! cannot survive it (#8374). `orca.cmd` refuses those subcommands and defers
//! here.
//!
//! Why not a managed assembly: a small freshly-compiled MSIL image in a
//! user-writable directory that mutates environment variables and proxies a
//! child process is the shape antivirus MSIL heuristics are trained on, and it
//! was flagged as exactly that across several vendors (#23383).
use std::env;
use std::path::{Path, PathBuf};
use std::process::{exit, Command};
fn main() {
let launcher = match env::current_exe() {
Ok(path) => path,
Err(error) => fail(&format!("Unable to start the Orca CLI: {error}")),
};
let Some(resources_directory) = launcher.parent().and_then(Path::parent) else {
fail(&format!(
"Unable to locate Orca.exe next to \"{}\"",
launcher.display()
))
};
let Some(app_directory) = resources_directory.parent() else {
fail(&format!(
"Unable to locate Orca.exe next to \"{}\"",
resources_directory.display()
))
};
let electron_path = app_directory.join("Orca.exe");
if !electron_path.is_file() {
fail(&format!(
"Unable to locate Orca.exe next to \"{}\"",
resources_directory.display()
));
}
let cli_path: PathBuf = resources_directory
.join("app.asar.unpacked")
.join("out")
.join("cli")
.join("index.js");
if !cli_path.is_file() {
fail(&format!(
"Unable to locate the Orca CLI entrypoint at \"{}\"",
cli_path.display()
));
}
// Why mutate this process rather than hand the child an environment map:
// an explicit map collapses a block carrying both PATH and Path into one
// entry, which is what killed the CLI in #12046. Leaving the map untouched
// makes the child inherit our block verbatim.
move_environment_variable("NODE_OPTIONS", "ORCA_NODE_OPTIONS");
move_environment_variable(
"NODE_REPL_EXTERNAL_MODULE",
"ORCA_NODE_REPL_EXTERNAL_MODULE",
);
env::set_var("ELECTRON_RUN_AS_NODE", "1");
env::set_var("ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER", "1");
let requested_command = env::var("ORCA_CLI_COMMAND").unwrap_or_default();
env::set_var(
"ORCA_CLI_COMMAND",
if requested_command == "orca-ide" {
"orca-ide"
} else {
"orca"
},
);
// Each argument stays its own argv entry, so a body holding newlines reaches
// the CLI intact.
let mut command = Command::new(&electron_path);
command.arg(&cli_path).args(env::args_os().skip(1));
match command.status() {
Ok(status) => exit(status.code().unwrap_or(1)),
Err(error) => fail(&format!("Unable to start the Orca CLI: {error}")),
}
}
fn move_environment_variable(source_name: &str, target_name: &str) {
match env::var_os(source_name) {
// A missing source clears the target, so a stale value cannot leak in.
None => env::remove_var(target_name),
Some(value) => {
env::remove_var(source_name);
env::set_var(target_name, value);
}
}
}
fn fail(message: &str) -> ! {
eprintln!("{message}");
exit(1)
}
+8 -12
View File
@@ -13,19 +13,15 @@ describe('packaged Windows CLI launcher asset', () => {
})
it('marks the packaged child and propagates its exact exit status', () => {
const sourcePath = join(process.cwd(), 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs')
const sourcePath = join(process.cwd(), 'native', 'windows-cli-launcher', 'src', 'main.rs')
const source = readFileSync(sourcePath, 'utf8')
// Why: the marker and command name must ride the launcher's own environment, never
// ProcessStartInfo's case-insensitive copy of a PATH/Path block (stablyai/orca#12046).
expect(source).toContain(
'Environment.SetEnvironmentVariable("ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER", "1");'
)
expect(source).toContain(
'string requestedCliCommand = Environment.GetEnvironmentVariable("ORCA_CLI_COMMAND");'
)
expect(source).toContain('requestedCliCommand == "orca-ide" ? "orca-ide" : "orca"')
expect(source).toContain('child.WaitForExit();')
expect(source).toContain('return child.ExitCode;')
// Why: the marker and command name must ride the launcher's own environment, never an
// explicit child map, whose case-insensitive keys collapse PATH and Path (stablyai/orca#12046).
expect(source).toContain('env::set_var("ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER", "1")')
expect(source).toContain('env::var("ORCA_CLI_COMMAND")')
expect(source).toContain('if requested_command == "orca-ide"')
expect(source).toContain('command.status()')
expect(source).toContain('exit(status.code().unwrap_or(1))')
})
})