@aiwg/cockpit 2026.7.21 → 2026.7.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -6
- package/bridge/src/server.mjs +426 -5
- package/bridge/src/smoke.mjs +44 -3
- package/package.json +1 -1
- package/web/dist/assets/index-BoRrjczI.js +312 -0
- package/web/dist/assets/index-CesGeDud.css +32 -0
- package/web/dist/index.html +2 -2
- package/web/src/App.test.tsx +34 -1
- package/web/src/components/Inventory.test.tsx +165 -0
- package/web/src/components/Inventory.tsx +176 -3
- package/web/src/components/LaunchInstanceModal.test.tsx +151 -1
- package/web/src/components/LaunchInstanceModal.tsx +193 -3
- package/web/src/components/Telemetry.tsx +17 -2
- package/web/src/runtimeContracts.fixtures.ts +230 -0
- package/web/src/runtimeContracts.test.ts +63 -0
- package/web/src/styles.css +7 -0
- package/web/src/types.ts +168 -2
- package/web/dist/assets/index-B0ea5aQ1.css +0 -32
- package/web/dist/assets/index-BRN8u5R_.js +0 -312
|
@@ -18,10 +18,103 @@ const HOST_INSTANCE = {
|
|
|
18
18
|
|
|
19
19
|
const LOADOUTS = [
|
|
20
20
|
{ id: 'host-tools', label: 'host-tools', description: 'Host tools', runtimes: ['host'] },
|
|
21
|
-
{
|
|
21
|
+
{
|
|
22
|
+
id: 'full-suite',
|
|
23
|
+
label: 'full-suite',
|
|
24
|
+
description: 'All providers',
|
|
25
|
+
runtimes: ['docker', 'container', 'qemu', 'vm'],
|
|
26
|
+
compatibility: [{ runtime_kind: 'vm', provider: 'cloud-hypervisor', eligible: true }],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'gpu-vfio',
|
|
30
|
+
label: 'GPU Workstation',
|
|
31
|
+
description: 'GPU-backed VM',
|
|
32
|
+
runtimes: ['qemu', 'vm'],
|
|
33
|
+
runtime_options: {
|
|
34
|
+
kind: 'vm',
|
|
35
|
+
provider: 'cloud-hypervisor',
|
|
36
|
+
required_capabilities: ['device.vfio'],
|
|
37
|
+
excluded_capabilities: ['instance.snapshot', 'instance.restore', 'instance.fork', 'warm_pool.manage'],
|
|
38
|
+
launch_strategy: { mode: 'cold' },
|
|
39
|
+
constraints: { allow_vfio_fast_start: false, fallback_mode: 'fail' },
|
|
40
|
+
},
|
|
41
|
+
compatibility: [{
|
|
42
|
+
runtime_kind: 'vm',
|
|
43
|
+
provider: 'cloud-hypervisor',
|
|
44
|
+
eligible: true,
|
|
45
|
+
required_capabilities: ['device.vfio'],
|
|
46
|
+
excluded_capabilities: ['instance.snapshot', 'instance.restore', 'instance.fork', 'warm_pool.manage'],
|
|
47
|
+
constraints: [{
|
|
48
|
+
capability: 'device.vfio',
|
|
49
|
+
excludes: ['instance.snapshot', 'instance.restore', 'instance.fork', 'warm_pool.manage'],
|
|
50
|
+
reason: 'VFIO-backed VMs cannot safely reuse memory state.',
|
|
51
|
+
}],
|
|
52
|
+
}],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'active-host-gpu',
|
|
56
|
+
label: 'Active Host GPU',
|
|
57
|
+
description: 'Rejected GPU assignment',
|
|
58
|
+
runtimes: ['qemu', 'vm'],
|
|
59
|
+
runtime_options: {
|
|
60
|
+
kind: 'vm',
|
|
61
|
+
provider: 'cloud-hypervisor',
|
|
62
|
+
required_capabilities: ['device.vfio'],
|
|
63
|
+
launch_strategy: { mode: 'cold' },
|
|
64
|
+
},
|
|
65
|
+
compatibility: [{
|
|
66
|
+
runtime_kind: 'vm',
|
|
67
|
+
provider: 'cloud-hypervisor',
|
|
68
|
+
eligible: false,
|
|
69
|
+
required_capabilities: ['device.vfio'],
|
|
70
|
+
reason: 'GPU 0000:41:00.0 is active on the host',
|
|
71
|
+
}],
|
|
72
|
+
},
|
|
22
73
|
{ id: 'agentic-dev', label: 'agentic-dev', description: 'Developer tools', runtimes: ['docker', 'container'] },
|
|
23
74
|
];
|
|
24
75
|
|
|
76
|
+
const VM_PROVIDER_CAPS = {
|
|
77
|
+
status: 'ok',
|
|
78
|
+
source: '/healthz/deep',
|
|
79
|
+
host_runtime_enabled: true,
|
|
80
|
+
runtime_providers: {
|
|
81
|
+
default_provider: 'cloud-hypervisor',
|
|
82
|
+
kinds: [{ kind: 'vm', label: 'VM', default_provider: 'cloud-hypervisor', providers: ['cloud-hypervisor'] }],
|
|
83
|
+
providers: [{
|
|
84
|
+
provider: 'cloud-hypervisor',
|
|
85
|
+
kind: 'vm',
|
|
86
|
+
label: 'Cloud Hypervisor',
|
|
87
|
+
default: true,
|
|
88
|
+
capabilities: [
|
|
89
|
+
{ id: 'instance.snapshot', label: 'Snapshot' },
|
|
90
|
+
{ id: 'instance.restore', label: 'Restore' },
|
|
91
|
+
{ id: 'instance.fork', label: 'Fork' },
|
|
92
|
+
{ id: 'warm_pool.manage', label: 'Warm pools' },
|
|
93
|
+
{ id: 'device.vfio', label: 'VFIO device passthrough' },
|
|
94
|
+
],
|
|
95
|
+
capability_constraints: [{
|
|
96
|
+
capability: 'device.vfio',
|
|
97
|
+
excludes: ['instance.snapshot', 'instance.restore', 'instance.fork', 'warm_pool.manage'],
|
|
98
|
+
reason: 'VFIO-backed VMs cannot safely reuse memory state.',
|
|
99
|
+
}],
|
|
100
|
+
}],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const VM_PROVIDER_CAPS_NO_GPU = {
|
|
105
|
+
...VM_PROVIDER_CAPS,
|
|
106
|
+
runtime_providers: {
|
|
107
|
+
...VM_PROVIDER_CAPS.runtime_providers,
|
|
108
|
+
providers: [{
|
|
109
|
+
provider: 'cloud-hypervisor',
|
|
110
|
+
kind: 'vm',
|
|
111
|
+
label: 'Cloud Hypervisor',
|
|
112
|
+
default: true,
|
|
113
|
+
capabilities: [{ id: 'instance.restore', label: 'Restore' }],
|
|
114
|
+
}],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
25
118
|
function mockFetch({
|
|
26
119
|
instances = [HOST_INSTANCE],
|
|
27
120
|
executorCaps = null,
|
|
@@ -143,4 +236,61 @@ describe('LaunchInstanceModal', () => {
|
|
|
143
236
|
ssh_key: '~/.ssh/agentic_ed25519.pub',
|
|
144
237
|
});
|
|
145
238
|
});
|
|
239
|
+
|
|
240
|
+
it('shows GPU unavailable when provider discovery does not advertise VFIO', async () => {
|
|
241
|
+
globalThis.fetch = mockFetch({ executorCaps: VM_PROVIDER_CAPS_NO_GPU });
|
|
242
|
+
render(<LaunchInstanceModal open onClose={() => {}} onLaunched={() => {}} />);
|
|
243
|
+
|
|
244
|
+
fireEvent.change(await screen.findByLabelText('Runtime'), { target: { value: 'qemu' } });
|
|
245
|
+
|
|
246
|
+
expect(await screen.findByText('GPU passthrough unavailable')).toBeTruthy();
|
|
247
|
+
expect((screen.getByRole('checkbox', { name: /request gpu passthrough/i }) as HTMLInputElement).disabled).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('rejects a GPU loadout when compatibility reports the host device is active', async () => {
|
|
251
|
+
globalThis.fetch = mockFetch({ executorCaps: VM_PROVIDER_CAPS });
|
|
252
|
+
render(<LaunchInstanceModal open onClose={() => {}} onLaunched={() => {}} />);
|
|
253
|
+
|
|
254
|
+
fireEvent.change(await screen.findByLabelText('Runtime'), { target: { value: 'qemu' } });
|
|
255
|
+
fireEvent.change(await screen.findByLabelText('Instance loadout'), { target: { value: 'active-host-gpu' } });
|
|
256
|
+
|
|
257
|
+
expect(await screen.findByText('GPU 0000:41:00.0 is active on the host')).toBeTruthy();
|
|
258
|
+
expect((screen.getByRole('button', { name: /create \+ start session/i }) as HTMLButtonElement).disabled).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('sends cold VFIO runtime_options for an eligible GPU loadout', async () => {
|
|
262
|
+
globalThis.fetch = mockFetch({ executorCaps: VM_PROVIDER_CAPS, createdInstanceId: 'vm-gpu-1' });
|
|
263
|
+
const onLaunched = vi.fn();
|
|
264
|
+
render(<LaunchInstanceModal open onClose={() => {}} onLaunched={onLaunched} />);
|
|
265
|
+
|
|
266
|
+
fireEvent.change(await screen.findByLabelText('Runtime'), { target: { value: 'qemu' } });
|
|
267
|
+
fireEvent.change(await screen.findByLabelText('Instance loadout'), { target: { value: 'gpu-vfio' } });
|
|
268
|
+
fireEvent.click(screen.getByRole('button', { name: /create \+ start session/i }));
|
|
269
|
+
|
|
270
|
+
await waitFor(() => expect(onLaunched).toHaveBeenCalledWith('vm-gpu-1', true, undefined));
|
|
271
|
+
const postCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls
|
|
272
|
+
.find((call) => String(call[0]).includes('/api/instances') && call[1]?.method === 'POST');
|
|
273
|
+
expect(JSON.parse(String(postCall?.[1]?.body))).toMatchObject({
|
|
274
|
+
runtime: 'qemu',
|
|
275
|
+
provider: 'cloud-hypervisor',
|
|
276
|
+
runtime_options: {
|
|
277
|
+
kind: 'vm',
|
|
278
|
+
provider: 'cloud-hypervisor',
|
|
279
|
+
required_capabilities: ['device.vfio'],
|
|
280
|
+
excluded_capabilities: ['instance.snapshot', 'instance.restore', 'instance.fork', 'warm_pool.manage'],
|
|
281
|
+
launch_strategy: { mode: 'cold' },
|
|
282
|
+
constraints: { allow_vfio_fast_start: false, fallback_mode: 'fail' },
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('surfaces VFIO fast-start incompatibility before launch', async () => {
|
|
288
|
+
globalThis.fetch = mockFetch({ executorCaps: VM_PROVIDER_CAPS });
|
|
289
|
+
render(<LaunchInstanceModal open onClose={() => {}} onLaunched={() => {}} />);
|
|
290
|
+
|
|
291
|
+
fireEvent.change(await screen.findByLabelText('Runtime'), { target: { value: 'qemu' } });
|
|
292
|
+
fireEvent.change(await screen.findByLabelText('Instance loadout'), { target: { value: 'gpu-vfio' } });
|
|
293
|
+
|
|
294
|
+
expect(await screen.findByText(/Disabled: instance.snapshot, instance.restore, instance.fork, warm_pool.manage/i)).toBeTruthy();
|
|
295
|
+
});
|
|
146
296
|
});
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { api } from '../api';
|
|
3
3
|
import { fmtId } from '../util';
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ExecutorCapabilities,
|
|
6
|
+
Instance,
|
|
7
|
+
Loadout,
|
|
8
|
+
ResolvedLoadoutCompatibility,
|
|
9
|
+
RuntimeOptions,
|
|
10
|
+
RuntimeProviderDescriptor,
|
|
11
|
+
SandboxRuntimeCapabilityId,
|
|
12
|
+
SandboxRuntimeProvider,
|
|
13
|
+
} from '../types';
|
|
5
14
|
|
|
6
15
|
type Runtime = 'host' | 'docker' | 'qemu';
|
|
7
16
|
|
|
@@ -31,6 +40,13 @@ const FALLBACK_LOADOUTS: Loadout[] = [
|
|
|
31
40
|
const genInstanceName = () =>
|
|
32
41
|
`cockpit-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 32);
|
|
33
42
|
|
|
43
|
+
const FAST_START_CAPABILITIES: SandboxRuntimeCapabilityId[] = [
|
|
44
|
+
'instance.snapshot',
|
|
45
|
+
'instance.restore',
|
|
46
|
+
'instance.fork',
|
|
47
|
+
'warm_pool.manage',
|
|
48
|
+
];
|
|
49
|
+
|
|
34
50
|
export function LaunchInstanceModal({
|
|
35
51
|
open,
|
|
36
52
|
onClose,
|
|
@@ -47,6 +63,8 @@ export function LaunchInstanceModal({
|
|
|
47
63
|
const [instances, setInstances] = useState<Instance[]>([]);
|
|
48
64
|
const [hostId, setHostId] = useState('');
|
|
49
65
|
const [executorCaps, setExecutorCaps] = useState<ExecutorCapabilities | null>(null);
|
|
66
|
+
const [vmProvider, setVmProvider] = useState('');
|
|
67
|
+
const [requestGpu, setRequestGpu] = useState(false);
|
|
50
68
|
const [image, setImage] = useState('agentic/codex:latest');
|
|
51
69
|
const [customImage, setCustomImage] = useState('');
|
|
52
70
|
const [profile, setProfile] = useState('');
|
|
@@ -79,8 +97,27 @@ export function LaunchInstanceModal({
|
|
|
79
97
|
return () => { cancelled = true; };
|
|
80
98
|
}, [open]);
|
|
81
99
|
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (!open || runtime !== 'qemu') return;
|
|
102
|
+
const currentLoadout = loadoutOptions(loadouts, runtime).find((item) => item.id === loadout);
|
|
103
|
+
const providers = vmProviderOptions(executorCaps, currentLoadout);
|
|
104
|
+
if (providers.length && !providers.some((provider) => provider.provider === vmProvider)) {
|
|
105
|
+
setVmProvider(providers[0].provider);
|
|
106
|
+
}
|
|
107
|
+
}, [executorCaps, loadout, loadouts, open, runtime, vmProvider]);
|
|
108
|
+
|
|
82
109
|
if (!open) return null;
|
|
83
110
|
|
|
111
|
+
const visibleLoadouts = loadoutOptions(loadouts, runtime);
|
|
112
|
+
const selectedLoadout = visibleLoadouts.find((item) => item.id === loadout);
|
|
113
|
+
const providerChoices = vmProviderOptions(executorCaps, selectedLoadout);
|
|
114
|
+
const selectedVmProvider = runtime === 'qemu'
|
|
115
|
+
? (vmProvider || providerChoices[0]?.provider || '')
|
|
116
|
+
: '';
|
|
117
|
+
const selectedProviderDescriptor = providerChoices.find((provider) => provider.provider === selectedVmProvider);
|
|
118
|
+
const selectedCompatibility = selectedVmProvider ? vmCompatibility(selectedLoadout, selectedVmProvider) : undefined;
|
|
119
|
+
const gpu = gpuLaunchPosture(selectedLoadout, selectedCompatibility, selectedProviderDescriptor, selectedVmProvider, requestGpu);
|
|
120
|
+
|
|
84
121
|
const chooseRuntime = (next: Runtime) => {
|
|
85
122
|
setRuntime(next);
|
|
86
123
|
if (next === 'host') {
|
|
@@ -90,6 +127,7 @@ export function LaunchInstanceModal({
|
|
|
90
127
|
setImage((current) => current || 'agentic/codex:latest');
|
|
91
128
|
} else {
|
|
92
129
|
setLoadout('profiles/basic.yaml');
|
|
130
|
+
setRequestGpu(false);
|
|
93
131
|
}
|
|
94
132
|
};
|
|
95
133
|
|
|
@@ -143,6 +181,9 @@ export function LaunchInstanceModal({
|
|
|
143
181
|
}
|
|
144
182
|
if (runtime === 'qemu') {
|
|
145
183
|
body.agentshare = true;
|
|
184
|
+
if (selectedVmProvider) body.provider = selectedVmProvider;
|
|
185
|
+
const runtimeOptions = runtimeOptionsForLaunch(selectedVmProvider, gpu);
|
|
186
|
+
if (runtimeOptions) body.runtime_options = runtimeOptions;
|
|
146
187
|
if (sshKey.trim()) body.ssh_key = sshKey.trim();
|
|
147
188
|
}
|
|
148
189
|
if (runtime === 'docker' && mounts.trim()) body.mounts = mounts.split('\n').map((m) => m.trim()).filter(Boolean);
|
|
@@ -206,9 +247,44 @@ export function LaunchInstanceModal({
|
|
|
206
247
|
<span className="ro">host-tools</span>
|
|
207
248
|
) : (
|
|
208
249
|
<select id="li-loadout" value={loadout} onChange={(e) => setLoadout(e.target.value)}>
|
|
209
|
-
{
|
|
250
|
+
{visibleLoadouts.map((l) => <option key={l.id} value={l.id}>{l.label}{l.description ? ` - ${l.description}` : ''}</option>)}
|
|
210
251
|
</select>
|
|
211
252
|
)}
|
|
253
|
+
{runtime === 'qemu' && (
|
|
254
|
+
<>
|
|
255
|
+
<label htmlFor="li-vm-provider">VM provider</label>
|
|
256
|
+
<select
|
|
257
|
+
id="li-vm-provider"
|
|
258
|
+
value={selectedVmProvider}
|
|
259
|
+
onChange={(e) => setVmProvider(e.target.value)}
|
|
260
|
+
disabled={!providerChoices.length}
|
|
261
|
+
>
|
|
262
|
+
{!providerChoices.length && <option value="">Provider discovery unavailable</option>}
|
|
263
|
+
{providerChoices.map((provider) => (
|
|
264
|
+
<option key={provider.provider} value={provider.provider}>
|
|
265
|
+
{provider.label ?? provider.provider}{provider.default ? ' (default)' : ''}
|
|
266
|
+
</option>
|
|
267
|
+
))}
|
|
268
|
+
</select>
|
|
269
|
+
<label htmlFor="li-gpu-vfio">GPU / VFIO</label>
|
|
270
|
+
<div className="field-stack">
|
|
271
|
+
<label className="check-row">
|
|
272
|
+
<input
|
|
273
|
+
id="li-gpu-vfio"
|
|
274
|
+
type="checkbox"
|
|
275
|
+
checked={gpu.required || requestGpu}
|
|
276
|
+
disabled={gpu.required || !gpu.selectable}
|
|
277
|
+
onChange={(e) => setRequestGpu(e.target.checked)}
|
|
278
|
+
/>
|
|
279
|
+
Request GPU passthrough
|
|
280
|
+
</label>
|
|
281
|
+
<div className={`posture-line ${gpu.blocked ? 'warn' : gpu.effective ? 'ok-text' : ''}`}>
|
|
282
|
+
{gpu.message}
|
|
283
|
+
</div>
|
|
284
|
+
{gpu.fastStartReason && <div className="cell-note">{gpu.fastStartReason}</div>}
|
|
285
|
+
</div>
|
|
286
|
+
</>
|
|
287
|
+
)}
|
|
212
288
|
{runtime !== 'host' && (
|
|
213
289
|
<>
|
|
214
290
|
<label htmlFor="li-profile">Profile</label>
|
|
@@ -255,7 +331,7 @@ export function LaunchInstanceModal({
|
|
|
255
331
|
</div>
|
|
256
332
|
<div className="modal-actions">
|
|
257
333
|
<button onClick={onClose} disabled={busy}>Close</button>
|
|
258
|
-
<button className="cta" onClick={launch} disabled={busy || (runtime === 'host' && !hostId && !executorCaps?.host_runtime_enabled) || (runtime === 'host' && !hostId && !name.trim()) || (runtime !== 'host' && !name.trim()) || (runtime === 'docker' && image === '__custom__' && !customImage.trim())}>
|
|
334
|
+
<button className="cta" onClick={launch} disabled={busy || (runtime === 'host' && !hostId && !executorCaps?.host_runtime_enabled) || (runtime === 'host' && !hostId && !name.trim()) || (runtime !== 'host' && !name.trim()) || (runtime === 'docker' && image === '__custom__' && !customImage.trim()) || (runtime === 'qemu' && gpu.effective && gpu.blocked)}>
|
|
259
335
|
{busy ? 'Working...' : runtime === 'host' ? (openSession ? 'Start host session' : 'Use host') : openSession ? 'Create + start session' : 'Create instance'}
|
|
260
336
|
</button>
|
|
261
337
|
</div>
|
|
@@ -264,6 +340,120 @@ export function LaunchInstanceModal({
|
|
|
264
340
|
);
|
|
265
341
|
}
|
|
266
342
|
|
|
343
|
+
function providerCapabilities(provider?: RuntimeProviderDescriptor) {
|
|
344
|
+
return new Set((provider?.capabilities ?? []).map((capability) => capability.id));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function requiredCapabilities(loadout?: Loadout, compatibility?: ResolvedLoadoutCompatibility) {
|
|
348
|
+
return new Set([
|
|
349
|
+
...(loadout?.runtime_options?.required_capabilities ?? []),
|
|
350
|
+
...(compatibility?.required_capabilities ?? []),
|
|
351
|
+
]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function excludedCapabilities(loadout?: Loadout, compatibility?: ResolvedLoadoutCompatibility) {
|
|
355
|
+
return new Set([
|
|
356
|
+
...(loadout?.runtime_options?.excluded_capabilities ?? []),
|
|
357
|
+
...(compatibility?.excluded_capabilities ?? []),
|
|
358
|
+
]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function vmProviderOptions(caps: ExecutorCapabilities | null, loadout?: Loadout): RuntimeProviderDescriptor[] {
|
|
362
|
+
const discovered = caps?.runtime_providers?.providers?.filter((provider) => provider.kind === 'vm') ?? [];
|
|
363
|
+
const compatible = new Set((loadout?.compatibility ?? [])
|
|
364
|
+
.filter((entry) => ['vm', 'qemu'].includes(String(entry.runtime_kind).toLowerCase()))
|
|
365
|
+
.map((entry) => entry.provider));
|
|
366
|
+
const filtered = compatible.size
|
|
367
|
+
? discovered.filter((provider) => compatible.has(provider.provider))
|
|
368
|
+
: discovered;
|
|
369
|
+
if (filtered.length) return filtered;
|
|
370
|
+
return [...compatible].map((provider) => ({
|
|
371
|
+
provider,
|
|
372
|
+
kind: 'vm',
|
|
373
|
+
capabilities: [],
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function vmCompatibility(loadout: Loadout | undefined, provider: SandboxRuntimeProvider): ResolvedLoadoutCompatibility | undefined {
|
|
378
|
+
return loadout?.compatibility?.find((entry) =>
|
|
379
|
+
entry.provider === provider && ['vm', 'qemu'].includes(String(entry.runtime_kind).toLowerCase()));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function gpuLaunchPosture(
|
|
383
|
+
loadout: Loadout | undefined,
|
|
384
|
+
compatibility: ResolvedLoadoutCompatibility | undefined,
|
|
385
|
+
provider: RuntimeProviderDescriptor | undefined,
|
|
386
|
+
providerId: string,
|
|
387
|
+
requested: boolean,
|
|
388
|
+
) {
|
|
389
|
+
const required = requiredCapabilities(loadout, compatibility).has('device.vfio');
|
|
390
|
+
const excluded = excludedCapabilities(loadout, compatibility);
|
|
391
|
+
const effective = required || requested;
|
|
392
|
+
const providerSupportsGpu = providerCapabilities(provider).has('device.vfio');
|
|
393
|
+
const fastStartExclusions = [...excluded].filter((capability) => FAST_START_CAPABILITIES.includes(capability));
|
|
394
|
+
const constraintReason = [
|
|
395
|
+
...(compatibility?.constraints ?? []),
|
|
396
|
+
...(provider?.capability_constraints ?? []),
|
|
397
|
+
].find((constraint) => constraint.capability === 'device.vfio')?.reason;
|
|
398
|
+
let blocked = false;
|
|
399
|
+
let message = 'No GPU requested';
|
|
400
|
+
|
|
401
|
+
if (!providerId) {
|
|
402
|
+
blocked = effective;
|
|
403
|
+
message = 'Provider discovery unavailable';
|
|
404
|
+
} else if (compatibility && !compatibility.eligible) {
|
|
405
|
+
blocked = effective || required;
|
|
406
|
+
message = compatibility.reason ?? 'Loadout is not eligible for this provider';
|
|
407
|
+
} else if (!compatibility && effective) {
|
|
408
|
+
blocked = true;
|
|
409
|
+
message = 'Loadout compatibility does not advertise VFIO';
|
|
410
|
+
} else if (!providerSupportsGpu && effective) {
|
|
411
|
+
blocked = true;
|
|
412
|
+
message = 'Selected provider does not advertise VFIO';
|
|
413
|
+
} else if (!providerSupportsGpu) {
|
|
414
|
+
message = 'GPU passthrough unavailable';
|
|
415
|
+
} else if (!compatibility) {
|
|
416
|
+
message = 'GPU passthrough requires loadout compatibility';
|
|
417
|
+
} else if (excluded.has('device.vfio') && effective) {
|
|
418
|
+
blocked = true;
|
|
419
|
+
message = 'Selected loadout excludes GPU passthrough';
|
|
420
|
+
} else if (required) {
|
|
421
|
+
message = blocked ? message : 'GPU passthrough required by loadout';
|
|
422
|
+
} else if (requested) {
|
|
423
|
+
message = blocked ? message : 'GPU passthrough requested';
|
|
424
|
+
} else if (providerSupportsGpu && !excluded.has('device.vfio')) {
|
|
425
|
+
message = 'GPU passthrough available';
|
|
426
|
+
} else if (excluded.has('device.vfio')) {
|
|
427
|
+
message = 'GPU passthrough unavailable for this loadout';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
required,
|
|
432
|
+
effective,
|
|
433
|
+
blocked,
|
|
434
|
+
selectable: Boolean(providerId && providerSupportsGpu && compatibility && !excluded.has('device.vfio') && compatibility.eligible !== false),
|
|
435
|
+
message,
|
|
436
|
+
fastStartReason: effective && fastStartExclusions.length
|
|
437
|
+
? `${constraintReason ?? 'VFIO-backed VMs cannot safely reuse memory state'} Disabled: ${fastStartExclusions.join(', ')}.`
|
|
438
|
+
: constraintReason,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function runtimeOptionsForLaunch(provider: string, gpu: ReturnType<typeof gpuLaunchPosture>): RuntimeOptions | undefined {
|
|
443
|
+
if (!provider && !gpu.effective) return undefined;
|
|
444
|
+
const options: RuntimeOptions = {
|
|
445
|
+
kind: 'vm',
|
|
446
|
+
launch_strategy: { mode: 'cold' },
|
|
447
|
+
};
|
|
448
|
+
if (provider) options.provider = provider as SandboxRuntimeProvider;
|
|
449
|
+
if (gpu.effective) {
|
|
450
|
+
options.required_capabilities = ['device.vfio'];
|
|
451
|
+
options.excluded_capabilities = FAST_START_CAPABILITIES;
|
|
452
|
+
options.constraints = { allow_vfio_fast_start: false, fallback_mode: 'fail' };
|
|
453
|
+
}
|
|
454
|
+
return options;
|
|
455
|
+
}
|
|
456
|
+
|
|
267
457
|
function loadoutOptions(loadouts: Loadout[], runtime: Runtime) {
|
|
268
458
|
if (runtime === 'host') return FALLBACK_LOADOUTS.filter((l) => l.id === 'host-tools');
|
|
269
459
|
const aliases = runtime === 'docker' ? ['docker', 'container'] : runtime === 'qemu' ? ['qemu', 'vm'] : [runtime];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { api } from '../api';
|
|
3
3
|
import { fmtId } from '../util';
|
|
4
|
-
import type { Cost, EventsSnapshot, MissionsResponse } from '../types';
|
|
4
|
+
import type { Cost, EventsSnapshot, McpDiscovery, MissionsResponse } from '../types';
|
|
5
5
|
|
|
6
6
|
type Filter = 'all' | 'mission' | 'task' | 'approval' | 'session' | 'inventory';
|
|
7
7
|
|
|
@@ -9,6 +9,7 @@ export function Telemetry({ refreshTick = 0 }: { refreshTick?: number }) {
|
|
|
9
9
|
const [events, setEvents] = useState<EventsSnapshot | null>(null);
|
|
10
10
|
const [missions, setMissions] = useState<MissionsResponse | null>(null);
|
|
11
11
|
const [cost, setCost] = useState<Cost | null>(null);
|
|
12
|
+
const [mcp, setMcp] = useState<McpDiscovery | null>(null);
|
|
12
13
|
const [filter, setFilter] = useState<Filter>('all');
|
|
13
14
|
const [err, setErr] = useState('');
|
|
14
15
|
|
|
@@ -17,10 +18,12 @@ export function Telemetry({ refreshTick = 0 }: { refreshTick?: number }) {
|
|
|
17
18
|
api<EventsSnapshot>('/api/events/snapshot'),
|
|
18
19
|
api<MissionsResponse>('/api/missions').catch(() => null),
|
|
19
20
|
api<Cost>('/api/cost').catch(() => null),
|
|
20
|
-
|
|
21
|
+
api<McpDiscovery>('/api/mcp/discovery').catch(() => null),
|
|
22
|
+
]).then(([eventData, missionData, costData, mcpData]) => {
|
|
21
23
|
setEvents(eventData);
|
|
22
24
|
setMissions(missionData);
|
|
23
25
|
setCost(costData);
|
|
26
|
+
setMcp(mcpData);
|
|
24
27
|
setErr('');
|
|
25
28
|
}).catch((e) => setErr((e as Error).message));
|
|
26
29
|
}, []);
|
|
@@ -45,8 +48,20 @@ export function Telemetry({ refreshTick = 0 }: { refreshTick?: number }) {
|
|
|
45
48
|
<article><span>Active Missions</span><strong>{missionTotals.active}</strong><small>{missionTotals.terminal} terminal</small></article>
|
|
46
49
|
<article><span>Approvals</span><strong>{missionTotals.awaiting}</strong><small>awaiting operator input</small></article>
|
|
47
50
|
<article><span>Spend</span><strong>{cost ? `$${cost.total.usd.toFixed(2)}` : '-'}</strong><small>{tokenTotal ? `${tokenTotal.toLocaleString()} tokens` : 'cost route unavailable'}</small></article>
|
|
51
|
+
<article><span>MCP</span><strong>{mcp?.status ?? '-'}</strong><small>{mcp ? `${mcp.tools.length} tools · ${mcp.auth.scopes.length} scopes` : 'discovery unavailable'}</small></article>
|
|
48
52
|
</section>
|
|
49
53
|
|
|
54
|
+
{mcp && (
|
|
55
|
+
<section className="audit-tail" aria-label="MCP management posture">
|
|
56
|
+
<h3>MCP Management</h3>
|
|
57
|
+
<p><strong>{mcp.endpoint.path}</strong> · {mcp.endpoint.transport} · {mcp.endpoint.stateless ? 'stateless' : 'stateful'} · session id {mcp.endpoint.mcp_session_id ? 'expected' : 'not expected'}</p>
|
|
58
|
+
<p>{mcp.auth.principals.length} principal(s): {mcp.auth.principals.map((principal) => principal.client_id).join(', ') || '-'}</p>
|
|
59
|
+
<p>Tools: {mcp.tools.map((tool) => tool.name).join(', ') || '-'}</p>
|
|
60
|
+
<p>Resources: {[...mcp.resources.map((resource) => resource.uri), ...mcp.resource_templates.map((template) => template.uriTemplate)].join(', ') || '-'}</p>
|
|
61
|
+
{mcp.reason_code && <p>{mcp.reason_code}</p>}
|
|
62
|
+
</section>
|
|
63
|
+
)}
|
|
64
|
+
|
|
50
65
|
<div className="controls" role="group" aria-label="Filter telemetry events">
|
|
51
66
|
{(['all', 'mission', 'task', 'approval', 'session', 'inventory'] as const).map((f) => (
|
|
52
67
|
<button key={f} aria-pressed={filter === f} onClick={() => setFilter(f)}>{f}</button>
|