import { useEffect, useState } from 'react'; import { api } from '../api'; import { fmtId } from '../util'; import type { ExecutorCapabilities, Instance, Loadout, ResolvedLoadoutCompatibility, RuntimeOptions, RuntimeProviderDescriptor, SandboxRuntimeCapabilityId, SandboxRuntimeProvider, } from '../types'; type Runtime = 'host' | 'docker' | 'qemu'; const DOCKER_IMAGE_OPTIONS = [ { value: 'agentic/codex:latest', label: 'Codex' }, { value: 'agentic/claude:latest', label: 'Claude' }, { value: 'agentic/opencode:latest', label: 'OpenCode' }, { value: 'agentic/automation-control:latest', label: 'Automation control' }, { value: 'agentic/agent:dev', label: 'Agent dev base' }, ]; const FALLBACK_LOADOUTS: Loadout[] = [ { id: 'host-tools', label: 'host-tools', description: 'Host tools', runtimes: ['host'] }, { id: 'agentic-dev', label: 'agentic-dev', description: 'Full development environment', runtimes: ['docker', 'container', 'qemu', 'vm'] }, { id: 'claude-only', label: 'claude-only', description: 'Claude provider loadout', runtimes: ['docker', 'container', 'qemu', 'vm'] }, { id: 'codex-only', label: 'codex-only', description: 'Codex provider loadout', runtimes: ['docker', 'container', 'qemu', 'vm'] }, { id: 'opencode-only', label: 'opencode-only', description: 'OpenCode provider loadout', runtimes: ['docker', 'container', 'qemu', 'vm'] }, { id: 'full-suite', label: 'full-suite', description: 'Multi-provider tool suite', runtimes: ['qemu', 'vm'] }, ]; // A fresh, collision-resistant instance name per launch. The executor keys // agents by instance name, so two instances sharing a name (e.g. a Docker // container and a VM both named `cockpit-`) register the same agent id and // shadow each other — the second launch silently knocks the first offline. // Date.now() alone collides within a page session (the name was generated once // at mount and reused); add random entropy and regenerate on every open/launch. const genInstanceName = () => `cockpit-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 32); const FAST_START_CAPABILITIES: SandboxRuntimeCapabilityId[] = [ 'instance.snapshot', 'instance.restore', 'instance.fork', 'warm_pool.manage', ]; export function LaunchInstanceModal({ open, onClose, onLaunched, }: { open: boolean; onClose: () => void; onLaunched: (instanceId?: string, openSession?: boolean, operationId?: string) => Promise | void; }) { const [runtime, setRuntime] = useState('host'); const [name, setName] = useState(genInstanceName); const [loadout, setLoadout] = useState('host-tools'); const [loadouts, setLoadouts] = useState([]); const [instances, setInstances] = useState([]); const [hostId, setHostId] = useState(''); const [executorCaps, setExecutorCaps] = useState(null); const [vmProvider, setVmProvider] = useState(''); const [requestGpu, setRequestGpu] = useState(false); const [image, setImage] = useState('agentic/codex:latest'); const [customImage, setCustomImage] = useState(''); const [profile, setProfile] = useState(''); const [sshKey, setSshKey] = useState(''); const [mounts, setMounts] = useState(''); const [openSession, setOpenSession] = useState(true); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); const [result, setResult] = useState(''); useEffect(() => { if (!open) return; // Fresh name every time the picker opens, so back-to-back launches (e.g. // Docker then VM) never collide on the executor's name-keyed agent registry. setName(genInstanceName()); let cancelled = false; Promise.all([ api<{ loadouts: Loadout[] }>('/api/loadouts').catch(() => ({ loadouts: [] as Loadout[] })), api<{ instances: Instance[] }>('/api/inventory').catch(() => ({ instances: [] as Instance[] })), api('/api/executor/capabilities').catch(() => null), ]) .then(([lo, inv, caps]) => { if (cancelled) return; setLoadouts(lo.loadouts ?? []); setInstances(inv.instances ?? []); setExecutorCaps(caps); const firstHost = (inv.instances ?? []).find(isUsableHost); setHostId((current) => current || firstHost?.id || ''); }); return () => { cancelled = true; }; }, [open]); useEffect(() => { if (!open || runtime !== 'qemu') return; const currentLoadout = loadoutOptions(loadouts, runtime).find((item) => item.id === loadout); const providers = vmProviderOptions(executorCaps, currentLoadout); if (providers.length && !providers.some((provider) => provider.provider === vmProvider)) { setVmProvider(providers[0].provider); } }, [executorCaps, loadout, loadouts, open, runtime, vmProvider]); if (!open) return null; const visibleLoadouts = loadoutOptions(loadouts, runtime); const selectedLoadout = visibleLoadouts.find((item) => item.id === loadout); const providerChoices = vmProviderOptions(executorCaps, selectedLoadout); const selectedVmProvider = runtime === 'qemu' ? (vmProvider || providerChoices[0]?.provider || '') : ''; const selectedProviderDescriptor = providerChoices.find((provider) => provider.provider === selectedVmProvider); const selectedCompatibility = selectedVmProvider ? vmCompatibility(selectedLoadout, selectedVmProvider) : undefined; const gpu = gpuLaunchPosture(selectedLoadout, selectedCompatibility, selectedProviderDescriptor, selectedVmProvider, requestGpu); const chooseRuntime = (next: Runtime) => { setRuntime(next); if (next === 'host') { setLoadout('host-tools'); } else if (next === 'docker') { setLoadout('agentic-dev'); setImage((current) => current || 'agentic/codex:latest'); } else { setLoadout('profiles/basic.yaml'); setRequestGpu(false); } }; const launch = async () => { setBusy(true); setErr(''); setResult(''); try { if (runtime === 'host') { const host = hostTargets(instances).find((i) => i.id === hostId) ?? hostTargets(instances)[0]; if (host) { setResult(openSession ? `Using host target ${host.launch_context?.name ?? fmtId(host.id)}; starting session...` : `Using host target ${host.launch_context?.name ?? fmtId(host.id)}`); await onLaunched(host.id, openSession); if (openSession) onClose(); return; } if (!executorCaps?.host_runtime_enabled) { throw new Error('Host runtime is not enabled on this executor. Enable the host supervisor or choose Docker container.'); } const body = { name: name.replace(/[^a-z0-9-]/g, '-').replace(/^-+/, 'a-').slice(0, 63), runtime: 'host', loadout: 'host-tools', start: true, }; const op = await api<{ id?: string; instance_id?: string; instanceId?: string; operation?: { id?: string }; result?: { instance_id?: string; instanceId?: string } }>('/api/instances', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const instanceId = op.instance_id ?? op.instanceId ?? op.result?.instance_id ?? op.result?.instanceId; const operationId = op.id ?? op.operation?.id; setResult(openSession ? `Host launch accepted: ${instanceId ?? operationId ?? 'operation pending'}; waiting for session...` : `Host launch accepted: ${instanceId ?? operationId ?? 'operation pending'}`); await onLaunched(instanceId, openSession, operationId); if (openSession) onClose(); else setName(genInstanceName()); return; } const body: Record = { name: name.replace(/[^a-z0-9-]/g, '-').replace(/^-+/, 'a-').slice(0, 63), runtime, start: true, }; if (loadout) body.loadout = loadout; if (profile) body.profile = profile; if (runtime === 'docker') { body.image = image === '__custom__' ? customImage.trim() : image; body.agentshare = true; } if (runtime === 'qemu') { body.agentshare = true; if (selectedVmProvider) body.provider = selectedVmProvider; const runtimeOptions = runtimeOptionsForLaunch(selectedVmProvider, gpu); if (runtimeOptions) body.runtime_options = runtimeOptions; if (sshKey.trim()) body.ssh_key = sshKey.trim(); } if (runtime === 'docker' && mounts.trim()) body.mounts = mounts.split('\n').map((m) => m.trim()).filter(Boolean); const op = await api<{ id?: string; instance_id?: string; instanceId?: string; operation?: { id?: string }; result?: { instance_id?: string; instanceId?: string } }>('/api/instances', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const instanceId = op.instance_id ?? op.instanceId ?? op.result?.instance_id ?? op.result?.instanceId; const operationId = op.id ?? op.operation?.id; setResult(openSession ? `Launch accepted: ${instanceId ?? operationId ?? 'operation pending'}; waiting for session...` : `Launch accepted: ${instanceId ?? operationId ?? 'operation pending'}`); await onLaunched(instanceId, openSession, operationId); if (openSession) onClose(); else setName(genInstanceName()); // modal stays open — next launch gets a fresh, non-colliding name } catch (e) { setErr((e as Error).message); } finally { setBusy(false); } }; return (
e.stopPropagation()}>

New instance

Create a runtime target. Existing instances and sessions keep running.

{err &&

{err}

} {result &&

{result}

}
{runtime === 'host' ? ( <> {!hostTargets(instances).length && executorCaps?.host_runtime_enabled && ( <> setName(e.target.value)} /> )} ) : ( <> setName(e.target.value)} /> )} {runtime === 'host' ? ( host-tools ) : ( )} {runtime === 'qemu' && ( <>
{gpu.message}
{gpu.fastStartReason &&
{gpu.fastStartReason}
}
)} {runtime !== 'host' && ( <> setProfile(e.target.value)} placeholder="optional" /> )} {runtime === 'qemu' && ( <> setSshKey(e.target.value)} placeholder="auto-detect, or ~/.ssh/agentic_ed25519.pub" /> )} {runtime === 'docker' && ( <>
{image === '__custom__' && ( setCustomImage(e.target.value)} placeholder="registry.example.com/team/agent:tag" /> )}