@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.
@@ -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
- { id: 'full-suite', label: 'full-suite', description: 'All providers', runtimes: ['docker', 'container', 'qemu', 'vm'] },
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 { ExecutorCapabilities, Instance, Loadout } from '../types';
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
- {loadoutOptions(loadouts, runtime).map((l) => <option key={l.id} value={l.id}>{l.label}{l.description ? ` - ${l.description}` : ''}</option>)}
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
- ]).then(([eventData, missionData, costData]) => {
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>