@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 +8 -5
- package/bridge/src/public/index.html +8 -1
- package/bridge/src/server.mjs +18 -4
- package/package.json +2 -1
- package/web/src/App.test.tsx +24 -0
- package/web/src/components/Inventory.tsx +12 -1
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.
|
|
386
|
-
|
|
387
|
-
- VM bootstrap/readiness is fixed from the agentic-sandbox side
|
|
388
|
-
`v2026.6.
|
|
389
|
-
|
|
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 {
|
|
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
|
|
package/bridge/src/server.mjs
CHANGED
|
@@ -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:
|
|
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.
|
|
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",
|
package/web/src/App.test.tsx
CHANGED
|
@@ -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
|
|
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>
|