@aiwg/cockpit 2026.6.11 → 2026.6.12

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 CHANGED
@@ -382,11 +382,14 @@ Known state as of the 2026-06-19 host live run:
382
382
  roctinam/agentic-sandbox#499 for upstream regression tracking, but it was not
383
383
  reproduced by this isolated host proof.
384
384
  - Docker/container secure bootstrap is fixed from the agentic-sandbox side in
385
- `v2026.6.24`; roctinam/agentic-sandbox#497 is closed. AIWG still needs to
386
- rerun this matrix against that release and record Cockpit-side evidence.
387
- - VM bootstrap/readiness is fixed from the agentic-sandbox side in
388
- `v2026.6.24`; roctinam/agentic-sandbox#498 is closed. AIWG still needs to
389
- rerun this matrix against that release and record Cockpit-side evidence.
385
+ `v2026.6.24`; roctinam/agentic-sandbox#497 is closed. Re-validated Cockpit-side
386
+ against `v2026.6.34`: container matrix PASS.
387
+ - VM bootstrap/readiness is fixed from the agentic-sandbox side via the vsock
388
+ transport line (`v2026.6.31`–`v2026.6.34`); roctinam/agentic-sandbox#498 and
389
+ the #561 transport regression are closed. Re-validated Cockpit-side against
390
+ `v2026.6.34`: VM matrix PASS — provision → vsock enroll → boot-ready → provider
391
+ workload → clean destroy. Evidence:
392
+ `.aiwg/testing/cockpit-vm-vsock-2026-06-27.md/.json`.
390
393
  - Remaining upstream follow-ups are Claude auth-state propagation
391
394
  (roctinam/agentic-sandbox#499) and agent-scoped PTY sessions not appearing in
392
395
  the formal/global session registry (roctinam/agentic-sandbox#500).
@@ -139,7 +139,14 @@
139
139
 
140
140
  // shared: POST/DELETE a control-plane call, then re-run a loader
141
141
  async function control(path, method, reload) {
142
- try { await api(path, { method }); } catch (e) { alert(e.message); }
142
+ try {
143
+ const res = await api(path, { method });
144
+ const body = await res.json().catch(() => ({}));
145
+ if (!res.ok) throw new Error(`${path} → ${res.status}`);
146
+ if (body.already_gone) {
147
+ meta.textContent = body.message || 'Instance already removed; inventory refreshed.';
148
+ }
149
+ } catch (e) { alert(e.message); }
143
150
  if (reload) reload();
144
151
  }
145
152
 
@@ -369,7 +369,22 @@ async function destroyInstance(upstreamUrl, instanceId) {
369
369
  }
370
370
  return result;
371
371
  }
372
- } catch {
372
+ } catch (err) {
373
+ const message = String(err?.message ?? err);
374
+ if (inst && / -> 404(?:;|$)/.test(message)) {
375
+ return {
376
+ target: `${upstreamUrl}/api/v2/admin/instances/${encodeURIComponent(instanceId)}/destroy`,
377
+ status: 200,
378
+ body: {
379
+ id: instanceId,
380
+ destroyed: instanceId,
381
+ state: 'destroyed',
382
+ result: { state: 'destroyed' },
383
+ already_gone: true,
384
+ message: `Instance ${instanceId} was already removed; inventory refreshed.`,
385
+ },
386
+ };
387
+ }
373
388
  // Current sandbox builds can list Docker rows in admin-v2 inventory while
374
389
  // lifecycle verbs return instance.not_found. Fall through to a dev cleanup.
375
390
  }
@@ -457,7 +472,7 @@ function normalizeRuntimePosture(kind) {
457
472
  label: 'Container / shared kernel',
458
473
  warning: 'Container isolation shares the host kernel.',
459
474
  };
460
- if (runtime === 'vm') return { kind: runtime, isolation: 'strong', label: 'VM / hardware boundary' };
475
+ if (runtime === 'vm' || runtime === 'qemu' || runtime === 'kvm') return { kind: 'vm', isolation: 'strong', label: 'VM / hardware boundary' };
461
476
  if (runtime === 'unknown') return { kind: runtime, isolation: 'unknown', label: 'Unknown runtime', warning: 'Runtime metadata was not reported by the sandbox.' };
462
477
  return {
463
478
  kind: runtime,
@@ -510,7 +525,7 @@ function normalizeSessionBackends(backends, runtimeKind, state = 'unknown', agen
510
525
  if (!list.length && runtimeKind === 'host') {
511
526
  return [{ mode: 'managed', backend: 'tmux', observe: true, drive: true, replay: false, keyframe: false, available: true, reason: 'agentic-sandbox v1 host session API default' }];
512
527
  }
513
- if (!list.length && ['docker', 'container', 'vm'].includes(runtimeKind) && String(state).toLowerCase() === 'running') {
528
+ if (!list.length && ['docker', 'container', 'vm', 'qemu', 'kvm'].includes(runtimeKind) && String(state).toLowerCase() === 'running') {
514
529
  return [{
515
530
  mode: 'managed',
516
531
  backend: 'tmux',
@@ -1166,7 +1181,6 @@ export function createBridge({ executorUrl = EXECUTOR_URL, allowMockExecutor = A
1166
1181
  session_class: mode || 'managed',
1167
1182
  command: 'bash',
1168
1183
  args: ['-l'],
1169
- working_dir: '/root',
1170
1184
  }),
1171
1185
  },
1172
1186
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwg/cockpit",
3
- "version": "2026.6.11",
3
+ "version": "2026.6.12",
4
4
  "description": "AIWG Cockpit — UX-first control plane over AIWG + multi-stack agentic sessions. Opt-in, separately published; NOT shipped in the base aiwg npm package (guarded by test/smoke/cockpit-base-footprint.test.js).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,6 +39,7 @@
39
39
  "scripts": {
40
40
  "start:bridge": "node bridge/src/server.mjs",
41
41
  "dev": "bash scripts/cockpit-dev.sh",
42
+ "up": "bash scripts/cockpit-up.sh",
42
43
  "build:web": "npm --prefix web install --no-audit --no-fund && npm --prefix web run build",
43
44
  "pack:dry": "npm pack --dry-run",
44
45
  "publish:dry": "npm publish --dry-run --access public",
@@ -195,6 +195,30 @@ describe('App shell (rendered DOM)', () => {
195
195
  expect(screen.getByText('Destination')).toBeTruthy();
196
196
  });
197
197
 
198
+ it('treats stale destroy 404 responses as already removed in Inventory (#1660)', async () => {
199
+ vi.spyOn(window, 'confirm').mockReturnValue(true);
200
+ const inventory = { instances: [instance('ghost-vm-1', 'vm', 'QEMU Codex')], count: 1, fetched_at: new Date().toISOString() };
201
+ globalThis.fetch = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
202
+ const url = String(input);
203
+ if (url.includes('/api/health')) return jsonResponse({ executor_url: 'http://127.0.0.1:8122' });
204
+ if (url.includes('/api/inventory')) return jsonResponse(inventory);
205
+ if (url.includes('/api/running')) return jsonResponse({ count: 0, running: [] });
206
+ if (url.includes('/api/approvals')) return jsonResponse({ approvals: [] });
207
+ if (url.includes('/api/cost')) return jsonResponse({ total: { input_tokens: 0, output_tokens: 0, usd: 0 }, per_instance: [] });
208
+ if (url.includes('/api/instances/ghost-vm-1') && init?.method === 'DELETE') {
209
+ return jsonResponse({ destroyed: 'ghost-vm-1', already_gone: true, message: 'Instance ghost-vm-1 was already removed; inventory refreshed.' });
210
+ }
211
+ return jsonResponse({});
212
+ }) as typeof fetch;
213
+
214
+ render(<App />);
215
+ fireEvent.click(screen.getByRole('tab', { name: 'Inventory' }));
216
+ fireEvent.click(await screen.findByRole('button', { name: /destroy instance ghost-vm-1/i }));
217
+
218
+ expect((await screen.findByRole('status')).textContent).toMatch(/already removed; inventory refreshed/i);
219
+ expect(screen.queryByText(/action failed/i)).toBeNull();
220
+ });
221
+
198
222
  it('each tab has a matching labelled tabpanel (controls/labelledby pairing)', () => {
199
223
  render(<App />);
200
224
  for (const tab of screen.getAllByRole('tab')) {
@@ -9,6 +9,7 @@ export function Inventory({ onStartSession, onLaunchInstance }: { onStartSession
9
9
  const [data, setData] = useState<Inv | null>(null);
10
10
  const [err, setErr] = useState('');
11
11
  const [actionErr, setActionErr] = useState('');
12
+ const [actionMsg, setActionMsg] = useState('');
12
13
 
13
14
  const load = useCallback(() => {
14
15
  api<Inv>('/api/inventory').then((d) => { setData(d); setErr(''); }).catch((e) => setErr((e as Error).message));
@@ -16,7 +17,16 @@ export function Inventory({ onStartSession, onLaunchInstance }: { onStartSession
16
17
  useEffect(() => { load(); }, [load]);
17
18
 
18
19
  const control = (path: string, method: string) =>
19
- api(path, { method }).then(() => { setActionErr(''); load(); }).catch((e) => setActionErr((e as Error).message));
20
+ api<{ already_gone?: boolean; message?: string }>(path, { method })
21
+ .then((result) => {
22
+ setActionErr('');
23
+ setActionMsg(result.already_gone ? (result.message ?? 'Instance already removed; inventory refreshed.') : '');
24
+ load();
25
+ })
26
+ .catch((e) => {
27
+ setActionMsg('');
28
+ setActionErr((e as Error).message);
29
+ });
20
30
 
21
31
  if (err) return <p className="err">Could not load inventory: {err}</p>;
22
32
  if (!data) return <p className="empty">Loading…</p>;
@@ -40,6 +50,7 @@ export function Inventory({ onStartSession, onLaunchInstance }: { onStartSession
40
50
  {onLaunchInstance && <button className="cta" onClick={onLaunchInstance}>+ New instance + session</button>}
41
51
  </div>
42
52
  {actionErr && <p className="err">Action failed: {actionErr}</p>}
53
+ {actionMsg && <p className="hint" role="status">{actionMsg}</p>}
43
54
  <table>
44
55
  <caption>Available instance deployments</caption>
45
56
  <thead>