feat(vm): add provisioned root recipe contract (#14352)

This commit is contained in:
Jinwoo Hong
2026-08-13 16:23:21 -07:00
committed by GitHub
parent a5a998ea77
commit 99d19d4635
18 changed files with 586 additions and 6 deletions
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest'
import {
getEphemeralVmRecipeCheckoutModeError,
getEphemeralVmRecipeResultSchemaVersion
} from './ephemeral-vm-recipe-checkout-mode'
import type { OrcaVmRecipe } from './types'
const defaultRecipe: OrcaVmRecipe = {
id: 'cloud-sandbox',
name: 'Cloud Sandbox',
create: './create.sh'
}
describe('ephemeral VM recipe checkout mode', () => {
it('keeps existing recipes on schema version 1', () => {
expect(getEphemeralVmRecipeResultSchemaVersion(defaultRecipe)).toBe(1)
expect(
getEphemeralVmRecipeCheckoutModeError(defaultRecipe, {
schemaVersion: 1,
pairingCode: 'orca://pair?code=test',
projectRoot: '/workspace/repo'
})
).toBeNull()
})
it('requires both sides to opt in to provisioned-root', () => {
const provisionedRootResult = {
schemaVersion: 2 as const,
checkoutMode: 'provisioned-root' as const,
pairingCode: 'orca://pair?code=test',
projectRoot: '/workspace/repo'
}
expect(getEphemeralVmRecipeCheckoutModeError(defaultRecipe, provisionedRootResult)).toBe(
'Recipe result requests provisioned-root checkout, but the recipe is not configured for it.'
)
expect(
getEphemeralVmRecipeCheckoutModeError(
{ ...defaultRecipe, checkoutMode: 'provisioned-root' },
provisionedRootResult
)
).toBeNull()
})
})
@@ -0,0 +1,21 @@
import { getEphemeralVmRecipeResultCheckoutMode } from './ephemeral-vm-recipes'
import type { EphemeralVmRecipeResult } from './ephemeral-vm-recipes'
import type { OrcaVmRecipe } from './types'
export function getEphemeralVmRecipeResultSchemaVersion(recipe: OrcaVmRecipe): 1 | 2 {
return recipe.checkoutMode === 'provisioned-root' ? 2 : 1
}
export function getEphemeralVmRecipeCheckoutModeError(
recipe: OrcaVmRecipe,
result: EphemeralVmRecipeResult
): string | null {
const configuredMode = recipe.checkoutMode ?? 'orca-worktree'
const resultMode = getEphemeralVmRecipeResultCheckoutMode(result)
if (configuredMode === resultMode) {
return null
}
return configuredMode === 'provisioned-root'
? 'Provisioned-root recipes must return schemaVersion 2 with checkoutMode "provisioned-root".'
: 'Recipe result requests provisioned-root checkout, but the recipe is not configured for it.'
}
@@ -38,6 +38,7 @@ describe('runRecipeCommand', () => {
command: nodeCommand(scriptPath),
repoPath,
mode: 'create',
resultSchemaVersion: 1,
context: {
recipeId: 'cloud-sandbox',
repoPath
@@ -71,6 +72,7 @@ describe('runRecipeCommand', () => {
command: nodeCommand(scriptPath),
repoPath,
mode: 'create',
resultSchemaVersion: 1,
context: {
recipeId: 'cloud-sandbox',
repoPath
+5 -2
View File
@@ -25,6 +25,7 @@ export async function runRecipeCommand(args: {
repoPath: string
context: EphemeralVmRecipeContext
mode: 'create' | 'suspend' | 'resume' | 'destroy'
resultSchemaVersion: 1 | 2
stdin?: string
env?: NodeJS.ProcessEnv
maxCaptureBytes?: number
@@ -42,7 +43,7 @@ export async function runRecipeCommand(args: {
child = spawnCommand(args.command, {
cwd: args.repoPath,
detached: process.platform !== 'win32',
env: buildRecipeEnv(args.env, args.mode, args.context),
env: buildRecipeEnv(args.env, args.mode, args.context, args.resultSchemaVersion),
shell: true,
windowsHide: true
}) as ChildProcessWithoutNullStreams
@@ -123,7 +124,8 @@ function killRecipeProcess(child: ChildProcessWithoutNullStreams): void {
function buildRecipeEnv(
env: NodeJS.ProcessEnv | undefined,
mode: 'create' | 'suspend' | 'resume' | 'destroy',
context: EphemeralVmRecipeContext
context: EphemeralVmRecipeContext,
resultSchemaVersion: 1 | 2
): NodeJS.ProcessEnv {
return {
...process.env,
@@ -138,6 +140,7 @@ function buildRecipeEnv(
ORCA_REPO_URL: context.repoUrl ?? '',
ORCA_REPO_BRANCH: context.branch ?? '',
ORCA_REPO_REF: context.ref ?? '',
ORCA_RECIPE_RESULT_SCHEMA_VERSION: String(resultSchemaVersion),
ORCA_VERSION: context.orcaVersion ?? ''
}
}
+30
View File
@@ -3,6 +3,10 @@ import { randomUUID } from 'node:crypto'
import { statSync } from 'node:fs'
import type { OrcaVmRecipe } from './types'
import { parseEphemeralVmRecipeResult, type EphemeralVmRecipeResult } from './ephemeral-vm-recipes'
import {
getEphemeralVmRecipeCheckoutModeError,
getEphemeralVmRecipeResultSchemaVersion
} from './ephemeral-vm-recipe-checkout-mode'
import { runRecipeCommand } from './ephemeral-vm-recipe-process'
import {
buildEphemeralVmRecipeCleanupPayload,
@@ -56,6 +60,7 @@ export type EphemeralVmRecipeStartFailure = {
stderr: string
exitCode: number | null
signal: NodeJS.Signals | null
recipeResult?: EphemeralVmRecipeResult
}
export type EphemeralVmRecipeStartResult =
@@ -108,6 +113,7 @@ export async function runEphemeralVmRecipeStart(
repoPath: args.repoPath,
context,
mode: 'create',
resultSchemaVersion: getEphemeralVmRecipeResultSchemaVersion(args.recipe),
env: args.env,
maxCaptureBytes: args.maxCaptureBytes,
signal: args.signal,
@@ -134,6 +140,16 @@ export async function runEphemeralVmRecipeStart(
...processResult
}
}
const checkoutModeError = getEphemeralVmRecipeCheckoutModeError(args.recipe, parsed.result)
if (checkoutModeError) {
return {
ok: false,
context,
error: checkoutModeError,
recipeResult: parsed.result,
...processResult
}
}
return {
ok: true,
@@ -158,6 +174,7 @@ export async function runEphemeralVmRecipeCleanup(
repoPath: args.repoPath,
context: args.context,
mode: 'destroy',
resultSchemaVersion: getEphemeralVmRecipeResultSchemaVersion(args.recipe),
stdin: `${JSON.stringify(payload)}\n`,
env: args.env,
maxCaptureBytes: args.maxCaptureBytes,
@@ -193,6 +210,7 @@ export async function runEphemeralVmRecipeSuspend(
repoPath: args.repoPath,
context: args.context,
mode: 'suspend',
resultSchemaVersion: getEphemeralVmRecipeResultSchemaVersion(args.recipe),
stdin: `${JSON.stringify(payload)}\n`,
env: args.env,
maxCaptureBytes: args.maxCaptureBytes,
@@ -234,6 +252,7 @@ export async function runEphemeralVmRecipeResume(
repoPath: args.repoPath,
context: args.context,
mode: 'resume',
resultSchemaVersion: getEphemeralVmRecipeResultSchemaVersion(args.recipe),
stdin: `${JSON.stringify(payload)}\n`,
env: args.env,
maxCaptureBytes: args.maxCaptureBytes,
@@ -263,6 +282,17 @@ export async function runEphemeralVmRecipeResume(
...processResult
}
}
const checkoutModeError = getEphemeralVmRecipeCheckoutModeError(args.recipe, parsed.result)
if (checkoutModeError) {
return {
ok: false,
skipped: false,
context: args.context,
error: checkoutModeError,
recipeResult: parsed.result,
...processResult
}
}
return {
ok: true,
+37
View File
@@ -127,6 +127,43 @@ describe('parseEphemeralVmRecipeResult', () => {
})
})
it('parses the provisioned-root version handshake', () => {
const result = parseEphemeralVmRecipeResult(
JSON.stringify({
schemaVersion: 2,
checkoutMode: 'provisioned-root',
connection: {
type: 'ssh',
projectRoot: 'C:\\workspace\\repo',
target: {
label: 'Sandbox',
host: 'sandbox.example.com',
port: 22,
username: 'root'
}
}
})
)
expect(result).toEqual({
ok: true,
result: {
schemaVersion: 2,
checkoutMode: 'provisioned-root',
connection: {
type: 'ssh',
projectRoot: 'C:\\workspace\\repo',
target: {
label: 'Sandbox',
host: 'sandbox.example.com',
port: 22,
username: 'root'
}
}
}
})
})
it('rejects ssh results with relative project roots', () => {
expect(
parseEphemeralVmRecipeResult(
+28 -1
View File
@@ -103,9 +103,30 @@ export const EphemeralVmRecipeConnectionResultSchema = z
})
.strict()
export const EphemeralVmRecipeProvisionedRootLegacyResultSchema = z
.object({
schemaVersion: z.literal(2),
checkoutMode: z.literal('provisioned-root'),
pairingCode: z.string().min(1),
projectRoot: z.string().min(1),
userData: z.record(z.string(), JsonValueSchema).optional()
})
.strict()
export const EphemeralVmRecipeProvisionedRootConnectionResultSchema = z
.object({
schemaVersion: z.literal(2),
checkoutMode: z.literal('provisioned-root'),
connection: EphemeralVmRecipeConnectionSchema,
userData: z.record(z.string(), JsonValueSchema).optional()
})
.strict()
export const EphemeralVmRecipeResultSchema = z.union([
EphemeralVmRecipeLegacyResultSchema,
EphemeralVmRecipeConnectionResultSchema
EphemeralVmRecipeConnectionResultSchema,
EphemeralVmRecipeProvisionedRootLegacyResultSchema,
EphemeralVmRecipeProvisionedRootConnectionResultSchema
])
export type EphemeralVmRecipeResult = z.infer<typeof EphemeralVmRecipeResultSchema>
@@ -173,6 +194,12 @@ export function getEphemeralVmRecipeResultProjectRoot(result: EphemeralVmRecipeR
return getEphemeralVmRecipeResultConnection(result).projectRoot
}
export function getEphemeralVmRecipeResultCheckoutMode(
result: EphemeralVmRecipeResult
): 'orca-worktree' | 'provisioned-root' {
return result.schemaVersion === 2 ? 'provisioned-root' : 'orca-worktree'
}
export function getEphemeralVmRecipeResultPairingCode(
result: EphemeralVmRecipeResult
): string | null {
+1
View File
@@ -32,6 +32,7 @@ const EphemeralVmRuntimeRecipeSchema = z
id: z.string().min(1),
name: z.string().min(1),
create: z.string().min(1),
checkoutMode: z.enum(['orca-worktree', 'provisioned-root']).optional(),
description: z.string().min(1).optional(),
suspend: z.string().min(1).optional(),
resume: z.string().min(1).optional(),
+10
View File
@@ -164,6 +164,15 @@ function normalizeVmRecipes(value: unknown): VmRecipeParseResult {
}
seenIds.add(id)
const description = asTrimmedString(record.description)
const checkoutMode = asTrimmedString(record.checkoutMode)
if (checkoutMode && checkoutMode !== 'orca-worktree' && checkoutMode !== 'provisioned-root') {
diagnostics.push({
index,
field: 'checkoutMode',
message: `Recipe "${id}" checkoutMode must be "orca-worktree" or "provisioned-root".`
})
return null
}
const suspend = asTrimmedString(record.suspend)
const resume = asTrimmedString(record.resume)
const destroyValue = asTrimmedString(record.destroy) ?? asTrimmedString(record.cleanup)
@@ -172,6 +181,7 @@ function normalizeVmRecipes(value: unknown): VmRecipeParseResult {
id,
name,
create,
...(checkoutMode ? { checkoutMode } : {}),
...(description ? { description } : {}),
...(suspend ? { suspend } : {}),
...(resume ? { resume } : {}),
@@ -11,6 +11,7 @@ describe('plugin VM recipe artifacts', () => {
schemaVersion: 1,
id: 'cloud-sandbox',
name: 'Cloud Sandbox',
checkoutMode: 'provisioned-root',
create: './scripts/create.sh',
suspend: './scripts/suspend.sh',
resume: './scripts/resume.sh',
@@ -18,7 +19,11 @@ describe('plugin VM recipe artifacts', () => {
})
)
expect(recipe).toMatchObject({ id: 'cloud-sandbox', name: 'Cloud Sandbox' })
expect(recipe).toMatchObject({
id: 'cloud-sandbox',
name: 'Cloud Sandbox',
checkoutMode: 'provisioned-root'
})
expect(listPluginVmRecipeCommands(recipe)).toEqual([
{ phase: 'create', command: './scripts/create.sh' },
{ phase: 'suspend', command: './scripts/suspend.sh' },
@@ -15,6 +15,7 @@ const pluginVmRecipeArtifactSchema = z
id: z.string().regex(ORCA_VM_RECIPE_ID_PATTERN, ORCA_VM_RECIPE_ID_RULE),
name: z.string().trim().min(1).max(128),
description: z.string().trim().min(1).max(1024).optional(),
checkoutMode: z.enum(['orca-worktree', 'provisioned-root']).optional(),
create: recipeCommandSchema,
suspend: recipeCommandSchema.optional(),
resume: recipeCommandSchema.optional(),
@@ -43,6 +44,7 @@ export function parsePluginVmRecipeArtifact(raw: string): OrcaVmRecipe {
id: parsed.id,
name: parsed.name,
create: parsed.create,
...(parsed.checkoutMode ? { checkoutMode: parsed.checkoutMode } : {}),
...(parsed.description ? { description: parsed.description } : {}),
...(parsed.suspend ? { suspend: parsed.suspend } : {}),
...(parsed.resume ? { resume: parsed.resume } : {}),
+3
View File
@@ -2218,10 +2218,13 @@ export type OrcaDefaultTabTemplate = {
command?: string
}
export type EphemeralVmCheckoutMode = 'orca-worktree' | 'provisioned-root'
export type OrcaVmRecipe = {
id: string
name: string
create: string
checkoutMode?: EphemeralVmCheckoutMode
description?: string
suspend?: string
resume?: string