Files
orca/src/shared/setup-agent-sequencing.ts
T
EotelandNeil c698f9bded fix: prevent setup command truncation during shell startup (#25820)
* fix: keep setup command short so it survives shell startup

The POSIX setup wrapper inlined the runner path six times. On long
worktree paths it exceeded macOS's 1024-byte canonical PTY input limit
when typed while the shell was still sourcing rc files, so the tail and
submit byte were dropped, setup never ran, and a gated agent waited
forever.

Carry the setup script in ORCA_SEQUENCED_SETUP_SCRIPT and submit only
`bash -lc 'eval "$ORCA_SEQUENCED_SETUP_SCRIPT"'`, mirroring the
existing startup gate. The env is merged into setup.envVars wherever the
gate is built so provisioning, fallback retries and renderer tabs/splits
all receive it, and WSL imports it untranslated.

Closes #20935

* fix(wsl): forward the setup script WSLENV entry before the daemon boundary

Same-protocol daemons from older builds rebuild WSLENV without
ORCA_SEQUENCED_SETUP_SCRIPT, so WSL setup would eval an empty script and
the gated agent would wait for its timeout. Also swap new spec casts for
the spawnedPtyEnv fixture.

* fix: fail loudly when the sequenced setup script env is missing

A dropped ORCA_SEQUENCED_SETUP_SCRIPT made eval run an empty string and
exit 0, silently skipping setup. ${VAR:?missing} reports it in the Setup pane.

---------

Co-authored-by: Neil <neil@stably.ai>
2026-10-28 00:41:15 -07:00

286 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { quotePosixShellUnlessPlainNoAt } from './shell-argument-quoting'
import { encodePowerShellCommand } from './powershell-command-encoding'
import { getPosixCodexShellLaunchPreflight } from './codex-shell-function'
import { getPosixClaudeShellFunction } from './claude-shell-function'
import {
nativeWindowsPathToPosixShellPath,
resolveSetupRunnerCommand,
type SetupRunnerCommandPlatform,
type SetupRunnerShell
} from './setup-runner-command'
import { createNonSecureContextUuid } from './non-secure-context-uuid'
import { quotePowerShellLiteral } from './powershell-native-argument'
const DEFAULT_WAIT_TIMEOUT_SECONDS = 2 * 60 * 60
export const SETUP_COMPLETE_MESSAGE = 'Setup finished; starting agent.'
export const SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV = 'ORCA_SEQUENCED_STARTUP_COMMAND'
export const SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV = 'ORCA_SEQUENCED_STARTUP_SCRIPT'
export const SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV = 'ORCA_SEQUENCED_SETUP_SCRIPT'
export type SequencedSetupAgentCommands = {
setupCommand: string
setupEnv?: Record<string, string>
startupCommand: string
startupEnv?: Record<string, string>
}
export function resolveSetupAgentSequenceLaunchCommand(
env: Record<string, string | undefined>,
fallbackCommand: string | undefined
): string | undefined {
const sequencedStartup = env[SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]?.trim()
return sequencedStartup || fallbackCommand
}
export function createSetupAgentSequenceNonce(): string {
return createNonSecureContextUuid()
}
export function createSequencedSetupAgentCommands(args: {
runnerScriptPath: string
startupCommand: string
platform: SetupRunnerCommandPlatform
shell?: SetupRunnerShell
nonce?: string
waitTimeoutSeconds?: number
}): SequencedSetupAgentCommands {
const nonce = args.nonce ?? createSetupAgentSequenceNonce()
const resolution = resolveSetupRunnerCommand(args.runnerScriptPath, args.platform, args.shell)
// startupCommand is already quoted for the pane: Git Bash needs a bash gate even for batch runners,
// which still launch through resolution.command. PowerShell cannot parse the POSIX escaping.
const posixGateForWindowsRunner = resolution.shell === 'windows' && args.shell?.family === 'posix'
const markerBasePath = posixGateForWindowsRunner
? nativeWindowsPathToPosixShellPath(resolution.runnerScriptPathForShell)
: resolution.runnerScriptPathForShell
// Use distinct markers for overlapping launches of one setup runner.
const markerPath = `${markerBasePath}.${nonce}.done`
const waitTimeoutSeconds = args.waitTimeoutSeconds ?? DEFAULT_WAIT_TIMEOUT_SECONDS
if (resolution.shell === 'windows' && !posixGateForWindowsRunner) {
return {
setupCommand: buildWindowsSetupCommand(
resolution.runnerScriptPathForShell,
markerPath,
nonce
),
startupCommand: buildWindowsStartupCommand(markerPath, nonce, waitTimeoutSeconds),
startupEnv: {
[SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: args.startupCommand
}
}
}
const startupScript = buildPosixStartupScript(
args.startupCommand,
markerPath,
nonce,
waitTimeoutSeconds
)
return {
// Why: long worktree paths can push typed wrappers past a PTY's canonical input cap and drop their submit byte.
setupCommand: `bash -lc 'eval "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:?missing}"'`,
setupEnv: {
[SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV]: buildPosixSetupScript(
resolution.command,
markerPath,
nonce
)
},
startupCommand: `bash -lc 'eval "$${SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV}"'`,
startupEnv: {
[SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV]: args.startupCommand,
[SETUP_AGENT_SEQUENCE_STARTUP_SCRIPT_ENV]: startupScript
}
}
}
function buildPosixSetupScript(setupCommand: string, markerPath: string, nonce: string): string {
const marker = quotePosixShellUnlessPlainNoAt(markerPath)
const tmp = quotePosixShellUnlessPlainNoAt(`${markerPath}.tmp`)
const nonceValue = quotePosixShellUnlessPlainNoAt(nonce)
return [
`rm -f ${marker} ${tmp} 2>/dev/null`,
setupCommand,
'status=$?',
`printf '%s:%s\\n' ${nonceValue} "$status" > ${tmp}`,
`mv -f ${tmp} ${marker}`,
'exit "$status"'
].join('; ')
}
function buildPosixStartupScript(
startupCommand: string,
markerPath: string,
nonce: string,
waitTimeoutSeconds: number
): string {
const marker = quotePosixShellUnlessPlainNoAt(markerPath)
const tmp = quotePosixShellUnlessPlainNoAt(`${markerPath}.tmp`)
const nonceValue = quotePosixShellUnlessPlainNoAt(nonce)
const timeout = Math.max(1, Math.floor(waitTimeoutSeconds))
const startupSuccessCommand = buildPosixStartupSuccessCommand(startupCommand)
// Keep the interactive wrapper one line to avoid visible quote-continuation prompts.
const script = [
`deadline=$((SECONDS + ${timeout}));`,
'echo "Waiting for setup to finish before starting agent..." >&2;',
'while :; do',
`if [ -f ${marker} ]; then`,
`IFS=: read -r seen status < ${marker} || true;`,
`if [ "$seen" = ${nonceValue} ]; then`,
`rm -f ${marker} ${tmp} 2>/dev/null;`,
// Announce success so “Waiting for setup...” does not linger. Install launch functions only after setup
// may have supplied codex; fresh bash -lc does not read Orca’s wrapper.
`if [ "$status" = "0" ]; then echo ${quotePosixShellUnlessPlainNoAt(SETUP_COMPLETE_MESSAGE)} >&2;\n${getPosixCodexShellLaunchPreflight() + getPosixClaudeShellFunction()}if [ -n "\${${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}:-}" ]; then eval "\$${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}"; exit "$?"; else ${startupSuccessCommand}; fi; fi;`,
'echo "Setup failed; skipping agent startup." >&2;',
'exit "${status:-1}";',
'fi;',
'fi;',
'if [ "$SECONDS" -ge "$deadline" ]; then',
'echo "Timed out waiting for setup before starting agent." >&2;',
'exit 124;',
'fi;',
'sleep 1;',
'done'
].join(' ')
return script
}
function buildPosixStartupSuccessCommand(startupCommand: string): string {
if (
hasUnquotedPosixCommandSeparator(startupCommand) ||
hasLeadingPosixEnvAssignment(startupCommand)
) {
return `eval ${quotePosixShellUnlessPlainNoAt(startupCommand)}; exit "$?"`
}
return `exec ${startupCommand}`
}
function hasLeadingPosixEnvAssignment(command: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(command.trimStart())
}
function hasUnquotedPosixCommandSeparator(command: string): boolean {
let quote: "'" | '"' | null = null
let escaped = false
for (const char of command) {
if (escaped) {
escaped = false
continue
}
if (char === '\\') {
escaped = true
continue
}
if (quote) {
if (char === quote) {
quote = null
}
continue
}
if (char === "'" || char === '"') {
quote = char
continue
}
if (char === ';' || char === '&' || char === '|' || char === '\n' || char === '\r') {
return true
}
}
return false
}
function buildWindowsSetupCommand(
runnerScriptPath: string,
markerPath: string,
nonce: string
): string {
// Why: delayed expansion keeps path metacharacters as data when cmd invokes the batch runner.
const script = [
`$runner = ${quotePowerShellLiteral(runnerScriptPath)}`,
`$marker = ${quotePowerShellLiteral(markerPath)}`,
'$tmp = $marker + ".tmp"',
`$nonce = ${quotePowerShellLiteral(nonce)}`,
'Remove-Item -LiteralPath $marker, $tmp -Force -ErrorAction SilentlyContinue',
'$processInfo = [System.Diagnostics.ProcessStartInfo]::new()',
'$processInfo.FileName = $env:ComSpec',
'$processInfo.Arguments = \'/d /s /v:on /c ""!ORCA_SETUP_RUNNER!""\'',
'$processInfo.UseShellExecute = $false',
'$processInfo.EnvironmentVariables["ORCA_SETUP_RUNNER"] = $runner',
'$process = [System.Diagnostics.Process]::Start($processInfo)',
'$process.WaitForExit()',
'$setupStatus = $process.ExitCode',
'$utf8 = [System.Text.UTF8Encoding]::new($false)',
'[System.IO.File]::WriteAllText($tmp, ($nonce + ":" + $setupStatus + [Environment]::NewLine), $utf8)',
'Move-Item -LiteralPath $tmp -Destination $marker -Force',
'exit $setupStatus'
].join('; ')
return encodePowerShellInvocation(script)
}
function buildWindowsStartupCommand(
markerPath: string,
nonce: string,
waitTimeoutSeconds: number
): string {
const timeout = Math.max(1, Math.floor(waitTimeoutSeconds))
// Use PowerShell for safe bounded marker polling; native Windows runners still launch through cmd.
const script = [
// User startup may invoke policy-gated .ps1; apply relief in-payload to avoid the flagged command-line switch.
// Silence/restore module-autoload progress so stderr stays clean. Stop routes nonterminating autoload errors
// to a reporting catch; never rethrow and abort startup over a broken security module.
"$orcaProgress = $ProgressPreference; $ProgressPreference = 'SilentlyContinue'",
'try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction Stop } ' +
'catch { [Console]::Error.WriteLine("Orca: could not relax the execution policy for this " + ' +
'"session (" + $_.FullyQualifiedErrorId + "). A startup command that runs a .ps1 " + ' +
'"may be blocked.") }',
'$ProgressPreference = $orcaProgress',
`$marker = ${quotePowerShellLiteral(markerPath)}`,
'if ([string]::IsNullOrWhiteSpace($marker)) {',
' [Console]::Error.WriteLine("Missing setup marker path.")',
' exit 1',
'}',
'$tmp = $marker + ".tmp"',
`$nonce = ${quotePowerShellLiteral(nonce)}`,
`$deadline = (Get-Date).AddSeconds(${timeout})`,
'[Console]::Error.WriteLine("Waiting for setup to finish before starting agent...")',
'while ($true) {',
' if (Test-Path -LiteralPath $marker) {',
' $content = Get-Content -LiteralPath $marker -TotalCount 1',
' if ($content -match "^([0-9A-Za-z_-]+):([0-9]+)$" -and $Matches[1] -eq $nonce) {',
' $setupStatus = [int]$Matches[2]',
' Remove-Item -LiteralPath $marker, $tmp -Force -ErrorAction SilentlyContinue',
' if ($setupStatus -ne 0) {',
' [Console]::Error.WriteLine("Setup failed; skipping agent startup.")',
' exit $setupStatus',
' }',
` $startup = $env:${SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV}`,
' if ([string]::IsNullOrWhiteSpace($startup)) {',
' [Console]::Error.WriteLine("Missing sequenced startup command.")',
' exit 1',
' }',
` [Console]::Error.WriteLine(${quotePowerShellLiteral(SETUP_COMPLETE_MESSAGE)})`,
' Invoke-Expression $startup',
' if ($global:LASTEXITCODE -ne $null) { exit $global:LASTEXITCODE }',
' if (-not $?) { exit 1 }',
' exit 0',
' }',
' }',
' if ((Get-Date) -ge $deadline) {',
' [Console]::Error.WriteLine("Timed out waiting for setup before starting agent.")',
' exit 124',
' }',
' Start-Sleep -Seconds 1',
'}'
].join('; ')
return encodePowerShellInvocation(script)
}
// EncodedCommand is not policy-gated; omit the EDR-heavy ExecutionPolicy Bypass switch.
// Keep base64 because terminal panes reparse the command.
function encodePowerShellInvocation(script: string): string {
return `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(script)}`
}