mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
fix(setup): fail closed when setup start is unverified
This commit is contained in:
@@ -4,3 +4,8 @@
|
||||
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'
|
||||
|
||||
/** Sentinel the setup half writes before running, shared by both platform gates. */
|
||||
export function setupStartedPath(markerPath: string): string {
|
||||
return `${markerPath}.started`
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV, SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from './setup-agent-sequencing-env'
|
||||
import {
|
||||
SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV,
|
||||
SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV,
|
||||
setupStartedPath
|
||||
} from './setup-agent-sequencing-env'
|
||||
|
||||
/** Shell fragments for the POSIX half of the setup-to-agent gate: the setup launch that records
|
||||
* an outcome, and the agent launch that waits for one. */
|
||||
export function setupStartedPath(markerPath: string): string {
|
||||
return `${markerPath}.started`
|
||||
}
|
||||
|
||||
export function buildPosixSetupCommand(bareRunnerCommand: string): string {
|
||||
const script = [
|
||||
`if [ -n "\${${SETUP_AGENT_SEQUENCE_SETUP_SCRIPT_ENV}:-}" ]; then`,
|
||||
@@ -17,7 +17,11 @@ export function buildPosixSetupCommand(bareRunnerCommand: string): string {
|
||||
return `bash -lc ${quotePosixArg(script)}`
|
||||
}
|
||||
|
||||
export function buildPosixSetupScript(setupCommand: string, markerPath: string, nonce: string): string {
|
||||
export function buildPosixSetupScript(
|
||||
setupCommand: string,
|
||||
markerPath: string,
|
||||
nonce: string
|
||||
): string {
|
||||
const marker = quotePosixArg(markerPath)
|
||||
const tmp = quotePosixArg(`${markerPath}.tmp`)
|
||||
const started = quotePosixArg(setupStartedPath(markerPath))
|
||||
@@ -79,11 +83,10 @@ export function buildPosixStartupScript(
|
||||
'echo "Setup started; waiting for it to finish." >&2;',
|
||||
'fi;',
|
||||
'if [ "$setup_started" = "0" ] && [ "$SECONDS" -ge "$start_deadline" ]; then',
|
||||
// Why: nothing reported starting, so we cannot claim setup failed — only that this terminal
|
||||
// has no way to observe it. Starting the agent unsequenced beats a terminal that waits out
|
||||
// the whole bound for an outcome no one is going to record.
|
||||
`echo "Setup never reported starting within ${grace}s, so this terminal cannot tell whether it ran. Starting the agent without waiting for setup." >&2;`,
|
||||
`${launchAgent}`,
|
||||
// Why: no start evidence is not success evidence. Stop here rather than allowing an
|
||||
// unsequenced agent to run against an environment whose setup outcome is unknown.
|
||||
`echo "Setup never reported starting within ${grace}s; the agent was not started because setup could not be verified. Open the Setup tab and retry once setup is running." >&2;`,
|
||||
'exit 125;',
|
||||
'fi;',
|
||||
'if [ "$SECONDS" -ge "$deadline" ]; then',
|
||||
`echo "Timed out waiting for setup before starting agent. Waited ${timeout}s without a result; the agent was not started. Open the Setup tab for its output." >&2;`,
|
||||
@@ -154,4 +157,3 @@ function quotePosixArg(value: string): string {
|
||||
}
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { encodePowerShellCommand } from './powershell-command-encoding'
|
||||
import { SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV } from './setup-agent-sequencing-env'
|
||||
import { setupStartedPath } from './setup-agent-sequencing-posix-gate'
|
||||
import {
|
||||
SETUP_AGENT_SEQUENCE_STARTUP_COMMAND_ENV,
|
||||
setupStartedPath
|
||||
} from './setup-agent-sequencing-env'
|
||||
|
||||
/** Shell fragments for the native-Windows half of the setup-to-agent gate. Both sides run under
|
||||
* PowerShell so the marker write and the bounded poll stay parseable without a batch label loop. */
|
||||
@@ -97,8 +99,8 @@ export function buildWindowsStartupCommand(
|
||||
' [Console]::Error.WriteLine("Setup started; waiting for it to finish.")',
|
||||
' }',
|
||||
' if (-not $setupStarted -and (Get-Date) -ge $startDeadline) {',
|
||||
` [Console]::Error.WriteLine("Setup never reported starting within ${grace}s, so this terminal cannot tell whether it ran. Starting the agent without waiting for setup.")`,
|
||||
' & $launchAgent',
|
||||
` [Console]::Error.WriteLine("Setup never reported starting within ${grace}s; the agent was not started because setup could not be verified. Open the Setup tab and retry once setup is running.")`,
|
||||
' exit 125',
|
||||
' }',
|
||||
' if ((Get-Date) -ge $deadline) {',
|
||||
` [Console]::Error.WriteLine("Timed out waiting for setup before starting agent. Waited ${timeout}s without a result; the agent was not started. Open the Setup tab for its output.")`,
|
||||
@@ -123,4 +125,3 @@ function encodePowerShellInvocation(script: string): string {
|
||||
function quotePowerShellString(value: string): string {
|
||||
return `'${value.replace(/'/g, "''")}'`
|
||||
}
|
||||
|
||||
|
||||
@@ -776,7 +776,7 @@ describe('setup outcome recording', () => {
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'starts the agent unsequenced when setup is never started at all',
|
||||
'fails closed when setup is never started at all',
|
||||
async () => {
|
||||
const tempDir = makeTempDir()
|
||||
const runnerScriptPath = join(tempDir, 'setup-runner.sh')
|
||||
@@ -801,10 +801,13 @@ describe('setup outcome recording', () => {
|
||||
})
|
||||
)
|
||||
|
||||
expect(startupExit.code).toBe(0)
|
||||
expect(startupExit.stderr).toContain('Setup never reported starting within 1s')
|
||||
expect(readFileSync(logPath, 'utf8')).toBe('agent-start\n')
|
||||
}
|
||||
expect(startupExit.code).toBe(125)
|
||||
expect(startupExit.stderr).toContain(
|
||||
'Setup never reported starting within 1s; the agent was not started'
|
||||
)
|
||||
expect(readIfExists(logPath)).toBe('')
|
||||
},
|
||||
15_000
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
@@ -836,7 +839,8 @@ describe('setup outcome recording', () => {
|
||||
})
|
||||
)
|
||||
// The wrapped launch record was dropped on the way to the setup terminal, so the gated
|
||||
// script is absent from its env; the fallback must still run setup.
|
||||
// script is absent from its env. Setup may still run, but the agent must fail closed because
|
||||
// no authoritative setup result was recorded.
|
||||
const setupExit = await waitForExit(
|
||||
spawn('bash', ['-lc', commands.setupCommand], { stdio: 'pipe', env: { ...process.env } })
|
||||
)
|
||||
@@ -844,10 +848,13 @@ describe('setup outcome recording', () => {
|
||||
|
||||
expect(setupExit.code).toBe(0)
|
||||
expect(readFileSync(setupLogPath, 'utf8')).toBe('setup-ran\n')
|
||||
expect(startupExit.code).toBe(0)
|
||||
expect(startupExit.stderr).toContain('Setup never reported starting within 2s')
|
||||
expect(readFileSync(logPath, 'utf8')).toBe('agent-start\n')
|
||||
}
|
||||
expect(startupExit.code).toBe(125)
|
||||
expect(startupExit.stderr).toContain(
|
||||
'Setup never reported starting within 2s; the agent was not started'
|
||||
)
|
||||
expect(readIfExists(logPath)).toBe('')
|
||||
},
|
||||
15_000
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
|
||||
@@ -29,9 +29,8 @@ import {
|
||||
const DEFAULT_WAIT_TIMEOUT_SECONDS = 30 * 60
|
||||
// Why: the setup terminal is spawned in the same host operation as the agent terminal and writes
|
||||
// its start sentinel before running a single line of the script, so anything past a slow shell
|
||||
// profile plus an SSH round trip means nobody is going to run setup at all. Expiring does not
|
||||
// fail the launch — it starts the agent unsequenced and says so — so a false positive costs a
|
||||
// warning line, never a dead terminal.
|
||||
// profile plus an SSH round trip means nobody is going to run setup at all. Expiring fails closed
|
||||
// with a clear retry message; an unknown setup outcome must never authorize an agent launch.
|
||||
const SETUP_START_GRACE_SECONDS = 45
|
||||
// Why: a silent terminal reads as a hang, so the wait reports itself on a human interval.
|
||||
const WAIT_PROGRESS_INTERVAL_SECONDS = 15
|
||||
|
||||
Reference in New Issue
Block a user