@aiwg/cockpit 2026.9.6 → 2026.9.7

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.
@@ -1,260 +1,347 @@
1
- // End-to-end data-path smoke: executor fixture (admin) -> Bridge (/api/inventory) -> served screen.
2
- // Self-contained (own ports); no deps. Exits non-zero on failure.
1
+ // Mock executor + real Bridge HTTP + strict CLI contract fixture + canonical corpus.
2
+ // Actual installed CLI discovery/show integration is a separate, unresolved lane.
3
3
  import assert from 'node:assert/strict';
4
4
  import { fileURLToPath } from 'node:url';
5
+ import { mkdtemp, mkdir, readFile, writeFile, rm } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
8
+ import { randomUUID } from 'node:crypto';
5
9
  import { createExecutor, DEFAULT_INSTANCE } from '../../mock-executor/src/server.mjs';
6
- import {
7
- createBridge,
8
- localLibvirtFallbackAllowed,
9
- normalizeSessionRows,
10
- selectCockpitA2AInterface,
11
- } from './server.mjs';
10
+ import { createBridge, isDirectExecution, localLibvirtFallbackAllowed, normalizeSessionRows, selectCockpitA2AInterface } from './server.mjs';
12
11
 
13
- const mock = createExecutor();
14
- await new Promise((r) => mock.listen(0, '127.0.0.1', r));
15
- const executorPort = mock.address().port;
16
- const executorUrl = `http://127.0.0.1:${executorPort}`;
12
+ const CORPUS = fileURLToPath(new URL('../../../../agentic/code', import.meta.url));
13
+ const SKILL_PATH = fileURLToPath(new URL('../../../../agentic/code/frameworks/sdlc-complete/skills/flow-deploy-to-production/SKILL.md', import.meta.url));
14
+ const CONTRIBUTIONS = fileURLToPath(new URL('../../contrib', import.meta.url));
17
15
 
18
- const bridge = createBridge({ executorUrl, allowMockExecutor: true });
19
- await new Promise((r) => bridge.listen(0, '127.0.0.1', r));
20
- const base = `http://127.0.0.1:${bridge.address().port}`;
21
- // authed fetch helper — every /api/ call carries the per-launch bearer token
22
- const f = (p, o = {}) => fetch(base + p, { ...o, headers: { ...(o.headers || {}), authorization: 'Bearer ' + bridge.cockpitToken } });
16
+ /** Contract double: never dispatch an unexpected command to the real CLI. */
17
+ export function createSmokeCliFixture(calls = []) {
18
+ return async (args) => {
19
+ calls.push([...args]);
20
+ if (JSON.stringify(args) === JSON.stringify(['discover', 'deploy production', '--json', '--limit', '4'])) {
21
+ return JSON.stringify({ results: [{ id: 'smoke:flow-deploy-to-production', name: 'flow-deploy-to-production', type: 'skill', path: SKILL_PATH }] });
22
+ }
23
+ if (JSON.stringify(args) === JSON.stringify(['show', 'skill', 'flow-deploy-to-production'])) return readFile(SKILL_PATH, 'utf8');
24
+ if (JSON.stringify(args) === JSON.stringify(['show', 'agent', '__definitely_not_a_real_artifact__'])) throw new Error('artifact not found: smoke missing-artifact fixture');
25
+ throw new Error(`Unexpected smoke CLI command: ${JSON.stringify(args)}`);
26
+ };
27
+ }
23
28
 
24
- try {
25
- // auth gate: /api/ without the token is 401; /healthz is open
26
- assert.equal((await fetch(`${base}/api/inventory`)).status, 401, 'gate: no token -> 401');
27
- assert.equal((await fetch(`${base}/api/inventory?token=${encodeURIComponent(bridge.cockpitToken)}`)).status, 401, 'gate: URL token is never accepted');
28
- assert.equal((await fetch(`${base}/healthz`)).status, 200, 'healthz open (no token)');
29
+ async function listen(server) {
30
+ await new Promise((resolve, reject) => {
31
+ server.once('error', reject);
32
+ server.listen(0, '127.0.0.1', () => { server.off('error', reject); resolve(); });
33
+ });
34
+ }
29
35
 
30
- // data path: Bridge reads the executor admin inventory
31
- const r = await f('/api/inventory');
32
- assert.equal(r.status, 200, 'inventory 200 (authed)');
33
- const inv = await r.json();
34
- assert.equal(inv.count, 4, 'four demo instances');
35
- const ids = inv.instances.map((i) => i.id);
36
- assert.ok(ids.includes('550e8400-e29b-41d4-a716-446655440000'), 'default instance present');
37
- assert.equal(inv.instances.find((i) => i.runtime === 'host')?.runtime_posture.isolation, 'least', 'host is least-isolated');
38
- assert.equal(inv.instances.find((i) => i.runtime === 'wasm-edge')?.runtime_posture.isolation, 'opaque', 'future runtime is opaque');
39
- assert.equal(inv.instances.find((i) => i.transport?.mode === 'shared-secret')?.transport.trust, 'compatibility', 'legacy secret transport is compatibility posture');
40
- const i0 = inv.instances[0];
41
- for (const k of ['id', 'runtime', 'loadout', 'state', 'tenant', 'card_url', 'runtime_posture', 'host_daemon', 'transport', 'launch_context', 'session_backends']) assert.ok(k in i0, `field ${k}`);
42
- assert.equal(i0.storage?.persistent, true, 'storage persistence surfaced');
43
- assert.equal(i0.storage?.delete_on_destroy, true, 'storage delete-on-destroy surfaced');
44
- assert.ok(['vm', 'container', 'host', 'wasm-edge'].includes(i0.runtime), 'runtime kind');
45
- assert.equal(i0.a2a_protocol?.selected_version, '0.3', 'inventory exposes the selected A2A protocol');
46
- assert.equal(selectCockpitA2AInterface({
47
- name: 'dual',
48
- version: '1',
49
- protocolVersion: '0.3.0',
50
- url: 'https://legacy.test/agent',
51
- supportedInterfaces: [
52
- { url: 'https://v1.test/agent', protocolBinding: 'HTTP+JSON', protocolVersion: '1.0' },
53
- { url: 'https://legacy.test/agent', transport: 'REST', protocolVersion: '0.3' },
54
- ],
55
- }, 'auto').protocol_version, '1.0', 'Cockpit auto selects the preferred 1.0 interface');
36
+ async function close(server) {
37
+ if (!server?.listening) return;
38
+ await new Promise((resolve, reject) => {
39
+ server.close((error) => error ? reject(error) : resolve());
40
+ server.closeIdleConnections?.();
41
+ });
42
+ }
56
43
 
57
- // A transient executor outage must not poison Bridge state or require a
58
- // Bridge restart. Every poll is a fresh upstream request, so the same Bridge
59
- // reports the gap and resumes inventory as soon as the executor returns.
60
- await new Promise((resolve) => mock.close(resolve));
61
- assert.equal((await f('/api/inventory')).status, 502, 'transient executor drop -> 502');
62
- await new Promise((resolve) => mock.listen(executorPort, '127.0.0.1', resolve));
63
- const recoveredInventory = await f('/api/inventory');
64
- assert.equal(recoveredInventory.status, 200, 'same Bridge resumes after executor returns');
65
- assert.equal((await recoveredInventory.json()).count, 4, 'recovered inventory is complete');
44
+ /** Own only the exact mkdtemp directory created here, including partial setup. */
45
+ export async function withSmokeFixture(callback, { temporaryParent = tmpdir() } = {}) {
46
+ const fixtureRoot = await mkdtemp(join(temporaryParent, 'aiwg-cockpit-smoke-'));
47
+ let mock, bridge, result, failure;
48
+ try {
49
+ const libraryDir = join(fixtureRoot, 'library'), auditDir = join(fixtureRoot, 'audit');
50
+ await mkdir(libraryDir);
51
+ await mkdir(auditDir);
52
+ // fs.cp checks destination ancestors up to the source parent. Keep clone
53
+ // source and library under the same owned root without granting /tmp reads.
54
+ // This is a canonical-content fixture, not installed-corpus clone coverage.
55
+ const cloneSourceDir = join(fixtureRoot, 'clone-source');
56
+ const skillPath = join(cloneSourceDir, 'SKILL.md');
57
+ await mkdir(cloneSourceDir);
58
+ await writeFile(skillPath, await readFile(SKILL_PATH), { flag: 'wx', mode: 0o600 });
59
+ const cliCalls = [];
60
+ mock = createExecutor({ protocolMode: '0.3' });
61
+ await listen(mock);
62
+ const executorPort = mock.address().port;
63
+ const executorUrl = `http://127.0.0.1:${executorPort}`;
64
+ bridge = createBridge({
65
+ executorUrl, allowMockExecutor: true, executorTokenFile: '', mcpTokenFile: '',
66
+ requireSandboxMtls: false, a2aProtocolPolicy: '0.3', allowA2AProtocolFallback: false,
67
+ localDockerFallback: false, localLibvirtFallback: false,
68
+ libraryDir, auditDir, corpusRoots: [CORPUS], contributionDirs: [CONTRIBUTIONS],
69
+ aiwgCommand: createSmokeCliFixture(cliCalls),
70
+ });
71
+ await listen(bridge);
72
+ const base = `http://127.0.0.1:${bridge.address().port}`;
73
+ const f = (p, o = {}) => fetch(base + p, { ...o, headers: { ...(o.headers || {}), authorization: 'Bearer ' + bridge.cockpitToken } });
74
+ result = await callback({ fixtureRoot, libraryDir, auditDir, mock, bridge, base, f, executorPort, executorUrl, cliCalls, skillPath });
75
+ } catch (error) { failure = error; }
76
+ const cleanupErrors = [];
77
+ for (const server of [bridge, mock]) {
78
+ try { await close(server); } catch (error) { cleanupErrors.push(error); }
79
+ }
80
+ try { await rm(fixtureRoot, { recursive: true, force: true }); } catch (error) { cleanupErrors.push(error); }
81
+ if (failure && cleanupErrors.length) throw new AggregateError([failure, ...cleanupErrors], 'Smoke failed and cleanup also failed', { cause: failure });
82
+ if (failure) throw failure;
83
+ if (cleanupErrors.length) throw new AggregateError(cleanupErrors, 'Smoke cleanup failed');
84
+ return result;
85
+ }
66
86
 
67
- // running board: seeded working tasks on the running instances
68
- const rr = await f("/api/running");
69
- assert.equal(rr.status, 200, 'running 200');
70
- const run = await rr.json();
71
- assert.ok(run.count >= 2, 'at least two running tasks seeded');
72
- for (const k of ['instance_id', 'task_id', 'state', 'tenant']) assert.ok(k in run.running[0], `running field ${k}`);
73
- for (const k of ['runtime_posture', 'transport']) assert.ok(k in run.running[0], `running posture field ${k}`);
74
- assert.equal(run.running[0].state, 'working', 'running task is working');
87
+ /** A 201 establishes ownership of this requested safe directory name, never a 400. */
88
+ export async function exerciseLibraryRoundTrip({ f, sourcePath, name, afterCreate = async () => {} }) {
89
+ assert.match(name, /^[a-zA-Z0-9][a-zA-Z0-9_-]+$/, 'smoke target name is safe and unchanged by library normalization');
90
+ let created = false, failure;
91
+ try {
92
+ const response = await f(`/api/library/clone?type=skill&name=${encodeURIComponent(name)}&path=${encodeURIComponent(sourcePath)}`, { method: 'POST' });
93
+ assert.equal(response.status, 201, 'clone returns 201 for a newly owned asset');
94
+ // Mark ownership before parsing: malformed response JSON must still trigger cleanup.
95
+ created = true;
96
+ const cloned = await response.json();
97
+ assert.equal(cloned.name, name, 'clone response names the newly owned asset');
98
+ assert.equal(cloned.type, 'skill', 'clone response preserves skill type');
99
+ assert.equal(cloned.kind, 'dir', 'smoke clones a skill directory');
100
+ const listed = await (await f('/api/library')).json();
101
+ assert.ok(listed.library.some((asset) => asset.name === name), 'cloned asset appears in the user library');
102
+ await afterCreate();
103
+ } catch (error) { failure = error; }
104
+ let cleanupError;
105
+ if (created) {
106
+ try {
107
+ assert.equal((await f(`/api/library/${encodeURIComponent(name)}`, { method: 'DELETE' })).status, 200, 'library delete 200');
108
+ const listed = await (await f('/api/library')).json();
109
+ assert.ok(!listed.library.some((asset) => asset.name === name), 'deleted asset removed from library');
110
+ } catch (error) { cleanupError = error; }
111
+ }
112
+ if (failure && cleanupError) throw new AggregateError([failure, cleanupError], 'Library smoke failed and cleanup also failed', { cause: failure });
113
+ if (failure) throw failure;
114
+ if (cleanupError) throw cleanupError;
115
+ }
75
116
 
76
- // sessions: the demo pty session is listed with a Bridge-owned ws attach_url
77
- const sr = await f("/api/sessions?instance=550e8400-e29b-41d4-a716-446655440000");
78
- assert.equal(sr.status, 200, 'sessions 200');
79
- const sess = await sr.json();
80
- const demo = sess.sessions.find((s) => s.id === 'demo-shell');
81
- assert.ok(demo, 'demo-shell session present');
82
- assert.match(demo.attach_url, /^ws:\/\/.*\/api\/pty\/agents\/.*\/sessions\/demo-shell\/attach\/[A-Za-z0-9_-]+$/, 'ws attach_url shape');
83
- assert.ok(demo.liveness.replay_newest_seq >= 3, 'demo session has a seeded transcript');
84
- assert.equal(demo.session_class, 'direct', 'demo session class');
85
- assert.equal(demo.session_backend, 'native', 'demo session backend');
86
- assert.equal(demo.role_policy, 'observe-default', 'session role policy');
117
+ export async function runBridgeSmoke(options = {}) {
118
+ return withSmokeFixture(async ({ mock, bridge, base, f, executorPort, executorUrl, skillPath }) => {
119
+ // auth gate: /api/ without the token is 401; /healthz is open
120
+ assert.equal((await fetch(`${base}/api/inventory`)).status, 401, 'gate: no token -> 401');
121
+ assert.equal((await fetch(`${base}/api/inventory?token=${encodeURIComponent(bridge.cockpitToken)}`)).status, 401, 'gate: URL token is never accepted');
122
+ assert.equal((await fetch(`${base}/healthz`)).status, 200, 'healthz open (no token)');
87
123
 
88
- const qemuDedup = normalizeSessionRows({
89
- executorUrl,
90
- instanceId: 'vm-1',
91
- sessionAgentId: 'vm-agent-name',
92
- sessions: [
93
- {
94
- id: 'sess-formal',
95
- session_id: 'sess-formal',
96
- command_id: 'cmd-real',
97
- session_name: 'terminal-qemu',
98
- command: '/bin/bash',
99
- has_screen: true,
100
- },
101
- {
102
- id: 'sess-formal',
103
- session_id: 'sess-formal',
104
- command_id: 'sess-formal',
105
- session_name: 'sess-formal',
106
- command: '/bin/bash -l',
107
- has_screen: false,
108
- },
109
- ],
110
- });
111
- assert.equal(qemuDedup.sessions.length, 1, 'QEMU formal session + fallback row dedupe to one session');
112
- assert.equal(qemuDedup.sessions[0].session_name, 'terminal-qemu', 'dedupe keeps the named screen-backed session');
124
+ // data path: Bridge reads the executor admin inventory
125
+ const r = await f('/api/inventory');
126
+ assert.equal(r.status, 200, 'inventory 200 (authed)');
127
+ const inv = await r.json();
128
+ assert.equal(inv.count, 4, 'four demo instances');
129
+ const ids = inv.instances.map((i) => i.id);
130
+ assert.ok(ids.includes('550e8400-e29b-41d4-a716-446655440000'), 'default instance present');
131
+ assert.equal(inv.instances.find((i) => i.runtime === 'host')?.runtime_posture.isolation, 'least', 'host is least-isolated');
132
+ assert.equal(inv.instances.find((i) => i.runtime === 'wasm-edge')?.runtime_posture.isolation, 'opaque', 'future runtime is opaque');
133
+ assert.equal(inv.instances.find((i) => i.transport?.mode === 'shared-secret')?.transport.trust, 'compatibility', 'legacy secret transport is compatibility posture');
134
+ const i0 = inv.instances[0];
135
+ for (const k of ['id', 'runtime', 'loadout', 'state', 'tenant', 'card_url', 'runtime_posture', 'host_daemon', 'transport', 'launch_context', 'session_backends']) assert.ok(k in i0, `field ${k}`);
136
+ assert.equal(i0.storage?.persistent, true, 'storage persistence surfaced');
137
+ assert.equal(i0.storage?.delete_on_destroy, true, 'storage delete-on-destroy surfaced');
138
+ assert.ok(['vm', 'container', 'host', 'wasm-edge'].includes(i0.runtime), 'runtime kind');
139
+ assert.equal(i0.a2a_protocol?.selected_version, '0.3', 'inventory exposes the selected A2A protocol');
140
+ assert.equal(selectCockpitA2AInterface({
141
+ name: 'dual',
142
+ version: '1',
143
+ protocolVersion: '0.3.0',
144
+ url: 'https://legacy.test/agent',
145
+ supportedInterfaces: [
146
+ { url: 'https://v1.test/agent', protocolBinding: 'HTTP+JSON', protocolVersion: '1.0' },
147
+ { url: 'https://legacy.test/agent', transport: 'REST', protocolVersion: '0.3' },
148
+ ],
149
+ }, 'auto').protocol_version, '1.0', 'Cockpit auto selects the preferred 1.0 interface');
113
150
 
114
- // missing instance param is a 400
115
- assert.equal((await f("/api/sessions")).status, 400, 'sessions requires instance');
151
+ // A transient executor outage must not poison Bridge state or require a
152
+ // Bridge restart. Every poll is a fresh upstream request, so the same Bridge
153
+ // reports the gap and resumes inventory as soon as the executor returns.
154
+ await new Promise((resolve) => mock.close(resolve));
155
+ assert.equal((await f('/api/inventory')).status, 502, 'transient executor drop -> 502');
156
+ await new Promise((resolve) => mock.listen(executorPort, '127.0.0.1', resolve));
157
+ const recoveredInventory = await f('/api/inventory');
158
+ assert.equal(recoveredInventory.status, 200, 'same Bridge resumes after executor returns');
159
+ assert.equal((await recoveredInventory.json()).count, 4, 'recovered inventory is complete');
116
160
 
117
- // loadout catalog passthrough — the start-session picker offers the full set (#1641)
118
- const lo = await (await f('/api/loadouts')).json();
119
- assert.ok(Array.isArray(lo.loadouts) && lo.loadouts.length >= 3, 'loadout catalog returned');
120
- assert.ok(lo.loadouts.every((l) => typeof l.id === 'string' && typeof l.label === 'string'), 'loadouts carry id+label');
121
- assert.ok(lo.loadouts.some((l) => l.id === 'security-audit'), 'catalog includes a non-default loadout');
122
- const gpuLoadout = lo.loadouts.find((l) => l.id === 'gpu-vfio');
123
- assert.ok(gpuLoadout?.runtime_options?.required_capabilities?.includes('device.vfio'), 'loadout runtime_options preserve VFIO requirement');
124
- assert.ok(gpuLoadout?.compatibility?.[0]?.excluded_capabilities?.includes('instance.restore'), 'loadout compatibility preserves fast-start exclusion');
161
+ // running board: seeded working tasks on the running instances
162
+ const rr = await f("/api/running");
163
+ assert.equal(rr.status, 200, 'running 200');
164
+ const run = await rr.json();
165
+ assert.ok(run.count >= 2, 'at least two running tasks seeded');
166
+ for (const k of ['instance_id', 'task_id', 'state', 'tenant']) assert.ok(k in run.running[0], `running field ${k}`);
167
+ for (const k of ['runtime_posture', 'transport']) assert.ok(k in run.running[0], `running posture field ${k}`);
168
+ assert.equal(run.running[0].state, 'working', 'running task is working');
125
169
 
126
- const caps = await (await f('/api/executor/capabilities')).json();
127
- assert.ok(caps.runtime_providers?.providers?.some((p) => p.provider === 'cloud-hypervisor'), 'runtime providers discovered');
128
- assert.ok(caps.runtime_providers.providers.find((p) => p.provider === 'cloud-hypervisor')?.capability_constraints?.[0]?.excludes?.includes('instance.restore'), 'provider VFIO constraint preserved');
129
- const hostProvider = caps.runtime_providers.providers.find((p) => p.provider === 'host');
130
- const dockerProvider = caps.runtime_providers.providers.find((p) => p.provider === 'docker');
131
- assert.ok(hostProvider?.platforms?.includes('darwin/arm64'), 'Apple Silicon host runtime discovery is proxied');
132
- assert.equal(hostProvider?.posture?.host_architecture, 'arm64', 'Apple Silicon host architecture is preserved');
133
- assert.equal(dockerProvider?.engine, 'Docker Desktop', 'Docker Desktop runtime posture is proxied');
134
- assert.equal(dockerProvider?.posture?.host_platform, 'darwin', 'Docker Desktop host platform is preserved');
135
- assert.equal(localLibvirtFallbackAllowed('darwin', undefined), false, 'virsh fallback is not automatic on macOS');
136
- assert.equal(localLibvirtFallbackAllowed('darwin', '1'), true, 'virsh fallback can be explicitly enabled for local development');
137
- assert.equal(localLibvirtFallbackAllowed('linux', undefined), true, 'Linux bridge hosts retain local virsh fallback');
170
+ // sessions: the demo pty session is listed with a Bridge-owned ws attach_url
171
+ const sr = await f("/api/sessions?instance=550e8400-e29b-41d4-a716-446655440000");
172
+ assert.equal(sr.status, 200, 'sessions 200');
173
+ const sess = await sr.json();
174
+ const demo = sess.sessions.find((s) => s.id === 'demo-shell');
175
+ assert.ok(demo, 'demo-shell session present');
176
+ assert.match(demo.attach_url, /^ws:\/\/.*\/api\/pty\/agents\/.*\/sessions\/demo-shell\/attach\/[A-Za-z0-9_-]+$/, 'ws attach_url shape');
177
+ assert.ok(demo.liveness.replay_newest_seq >= 3, 'demo session has a seeded transcript');
178
+ assert.equal(demo.session_class, 'direct', 'demo session class');
179
+ assert.equal(demo.session_backend, 'native', 'demo session backend');
180
+ assert.equal(demo.role_policy, 'observe-default', 'session role policy');
138
181
 
139
- const vm = inv.instances.find((i) => i.provider === 'cloud-hypervisor');
140
- assert.ok(vm, 'provider-aware VM inventory row present');
141
- const fastStartAccepted = await (await f(`/api/instances/${encodeURIComponent(vm.id)}/snapshot`, {
142
- method: 'POST',
143
- headers: { 'content-type': 'application/json' },
144
- body: JSON.stringify({ asset_ref: 'cockpit-smoke-snapshot' }),
145
- })).json();
146
- assert.ok(fastStartAccepted.id, 'fast-start proxy returns operation id');
147
- const fastStartTerminal = await (await f(`/api/operations/${encodeURIComponent(fastStartAccepted.id)}`)).json();
148
- assert.equal(fastStartTerminal.state, 'succeeded', 'fast-start operation reaches terminal state');
149
- assert.equal(fastStartTerminal.result.provider, 'cloud-hypervisor', 'fast-start operation preserves provider');
182
+ const qemuDedup = normalizeSessionRows({
183
+ executorUrl,
184
+ instanceId: 'vm-1',
185
+ sessionAgentId: 'vm-agent-name',
186
+ sessions: [
187
+ {
188
+ id: 'sess-formal',
189
+ session_id: 'sess-formal',
190
+ command_id: 'cmd-real',
191
+ session_name: 'terminal-qemu',
192
+ command: '/bin/bash',
193
+ has_screen: true,
194
+ },
195
+ {
196
+ id: 'sess-formal',
197
+ session_id: 'sess-formal',
198
+ command_id: 'sess-formal',
199
+ session_name: 'sess-formal',
200
+ command: '/bin/bash -l',
201
+ has_screen: false,
202
+ },
203
+ ],
204
+ });
205
+ assert.equal(qemuDedup.sessions.length, 1, 'QEMU formal session + fallback row dedupe to one session');
206
+ assert.equal(qemuDedup.sessions[0].session_name, 'terminal-qemu', 'dedupe keeps the named screen-backed session');
150
207
 
151
- const mcp = await (await f('/api/mcp/discovery')).json();
152
- assert.equal(mcp.enabled, true, 'MCP discovery enabled');
153
- assert.equal(mcp.endpoint?.path, '/mcp', 'MCP endpoint path surfaced');
154
- assert.equal(mcp.endpoint?.mcp_session_id, false, 'MCP discovery is stateless/no session id');
155
- assert.ok(mcp.tools.some((tool) => tool.name === 'list_sandboxes'), 'MCP tools surfaced');
156
- assert.ok(mcp.resource_templates.some((template) => template.uriTemplate === 'sandbox://sessions/{session_id}/screen'), 'MCP resource templates surfaced');
157
- assert.equal((await f('/api/mcp', { method: 'POST', body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) })).status, 503, 'MCP proxy fail-closed without token file');
158
- const gatedReconnect = await f(`/api/instances/${DEFAULT_INSTANCE}/reconnect`, { method: 'POST' });
159
- assert.equal(gatedReconnect.status, 409, 'Docker reconnect fallback is gated unless local dev flag is set');
160
- assert.equal((await gatedReconnect.json()).error, 'local_docker_fallback_disabled', 'Docker reconnect fallback error is explicit');
208
+ // missing instance param is a 400
209
+ assert.equal((await f("/api/sessions")).status, 400, 'sessions requires instance');
161
210
 
162
- // registry binding: discover + show through the aiwg CLI (#1592)
163
- const cap = await (await f("/api/capabilities?q=" + encodeURIComponent("deploy production") + "&limit=4")).json();
164
- assert.ok(Array.isArray(cap.results) && cap.results.length >= 1, 'discover returns results');
165
- const hit = cap.results.find((r) => r.name === 'flow-deploy-to-production');
166
- assert.ok(hit, 'flow-deploy-to-production discoverable');
167
- assert.ok(hit.name && hit.type, 'result carries name+type for show');
168
- const shown = await (await f("/api/show?type=skill&name=flow-deploy-to-production")).json();
169
- assert.match(shown.body, /name:\s*flow-deploy-to-production/, 'show returns the skill body');
170
- assert.equal((await f("/api/capabilities")).status, 400, 'capabilities requires q');
171
- // show by discovered PATH — deterministic, sidesteps ambiguous same-named artifacts (#1643)
172
- if (hit.path) {
173
- const shownByPath = await (await f(`/api/show?path=${encodeURIComponent(hit.path)}`)).json();
174
- assert.match(shownByPath.body, /name:\s*flow-deploy-to-production/, 'show-by-path returns the body');
175
- assert.equal(shownByPath.path, hit.path, 'show-by-path echoes the resolved path');
176
- } else {
177
- assert.ok(hit.id, 'pathless discover result carries a stable id');
178
- }
179
- // a missing artifact is a 4xx, never a 502 (ambiguous/not-found map to operator-correctable input)
180
- assert.equal((await f('/api/show?type=agent&name=__definitely_not_a_real_artifact__')).status, 404, 'unknown artifact -> 404 not 502');
181
- // a path outside the AIWG corpus is refused (no traversal)
182
- assert.equal((await f(`/api/show?path=${encodeURIComponent('/etc/passwd')}`)).status, 400, 'path outside corpus -> 400');
211
+ // loadout catalog passthrough — the start-session picker offers the full set (#1641)
212
+ const lo = await (await f('/api/loadouts')).json();
213
+ assert.ok(Array.isArray(lo.loadouts) && lo.loadouts.length >= 3, 'loadout catalog returned');
214
+ assert.ok(lo.loadouts.every((l) => typeof l.id === 'string' && typeof l.label === 'string'), 'loadouts carry id+label');
215
+ assert.ok(lo.loadouts.some((l) => l.id === 'security-audit'), 'catalog includes a non-default loadout');
216
+ const gpuLoadout = lo.loadouts.find((l) => l.id === 'gpu-vfio');
217
+ assert.ok(gpuLoadout?.runtime_options?.required_capabilities?.includes('device.vfio'), 'loadout runtime_options preserve VFIO requirement');
218
+ assert.ok(gpuLoadout?.compatibility?.[0]?.excluded_capabilities?.includes('instance.restore'), 'loadout compatibility preserves fast-start exclusion');
183
219
 
184
- // contribution model: actions INJECT a command into a session — the Cockpit never
185
- // runs the CLI (adr-cockpit-session-control-not-cli-runner) (#1591)
186
- const contrib = await (await f("/api/contributions")).json();
187
- assert.ok(contrib.sources.some((s) => s.id === 'aiwg-core'), 'aiwg-core contribution loaded');
188
- const audit = contrib.actions.find((a) => a.id === 'audit-issues');
189
- assert.ok(audit && typeof audit.inject.command === 'string', 'audit-issues declares an inject command');
190
- assert.match(audit.inject.command, /issue-audit/, 'audit-issues injects the issue-audit command');
191
- // the spawn-aiwg run endpoint is removed
192
- assert.equal((await f("/api/actions/audit-issues/run", { method: 'POST' })).status, 404, 'action run endpoint gone (no Bridge CLI run for actions)');
220
+ const caps = await (await f('/api/executor/capabilities')).json();
221
+ assert.ok(caps.runtime_providers?.providers?.some((p) => p.provider === 'cloud-hypervisor'), 'runtime providers discovered');
222
+ assert.ok(caps.runtime_providers.providers.find((p) => p.provider === 'cloud-hypervisor')?.capability_constraints?.[0]?.excludes?.includes('instance.restore'), 'provider VFIO constraint preserved');
223
+ const hostProvider = caps.runtime_providers.providers.find((p) => p.provider === 'host');
224
+ const dockerProvider = caps.runtime_providers.providers.find((p) => p.provider === 'docker');
225
+ assert.ok(hostProvider?.platforms?.includes('darwin/arm64'), 'Apple Silicon host runtime discovery is proxied');
226
+ assert.equal(hostProvider?.posture?.host_architecture, 'arm64', 'Apple Silicon host architecture is preserved');
227
+ assert.equal(dockerProvider?.engine, 'Docker Desktop', 'Docker Desktop runtime posture is proxied');
228
+ assert.equal(dockerProvider?.posture?.host_platform, 'darwin', 'Docker Desktop host platform is preserved');
229
+ assert.equal(localLibvirtFallbackAllowed('darwin', undefined), false, 'virsh fallback is not automatic on macOS');
230
+ assert.equal(localLibvirtFallbackAllowed('darwin', '1'), true, 'virsh fallback can be explicitly enabled for local development');
231
+ assert.equal(localLibvirtFallbackAllowed('linux', undefined), true, 'Linux bridge hosts retain local virsh fallback');
193
232
 
194
- // management: lifecycle (UC-012)
195
- const stoppedId = '9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b';
196
- assert.equal((await (await f(`/api/instances/${stoppedId}/start`, { method: 'POST' })).json()).state, 'running', 'start -> running');
197
- assert.equal((await (await f(`/api/instances/${stoppedId}/stop`, { method: 'POST' })).json()).state, 'stopped', 'stop -> stopped');
233
+ const vm = inv.instances.find((i) => i.provider === 'cloud-hypervisor');
234
+ assert.ok(vm, 'provider-aware VM inventory row present');
235
+ const fastStartAccepted = await (await f(`/api/instances/${encodeURIComponent(vm.id)}/snapshot`, {
236
+ method: 'POST',
237
+ headers: { 'content-type': 'application/json' },
238
+ body: JSON.stringify({ asset_ref: 'cockpit-smoke-snapshot' }),
239
+ })).json();
240
+ assert.ok(fastStartAccepted.id, 'fast-start proxy returns operation id');
241
+ const fastStartTerminal = await (await f(`/api/operations/${encodeURIComponent(fastStartAccepted.id)}`)).json();
242
+ assert.equal(fastStartTerminal.state, 'succeeded', 'fast-start operation reaches terminal state');
243
+ assert.equal(fastStartTerminal.result.provider, 'cloud-hypervisor', 'fast-start operation preserves provider');
198
244
 
199
- // management: cancel a running task
200
- const before = await (await f('/api/running')).json();
201
- const victim = before.running[0];
202
- assert.equal((await f(`/api/tasks/${victim.instance_id}/${victim.task_id}/cancel`, { method: 'POST' })).status, 200, 'task cancel 200');
203
- const after = await (await f('/api/running')).json();
204
- assert.ok(after.count < before.count, 'cancel removed a running task');
245
+ const mcp = await (await f('/api/mcp/discovery')).json();
246
+ assert.equal(mcp.enabled, true, 'MCP discovery enabled');
247
+ assert.equal(mcp.endpoint?.path, '/mcp', 'MCP endpoint path surfaced');
248
+ assert.equal(mcp.endpoint?.mcp_session_id, false, 'MCP discovery is stateless/no session id');
249
+ assert.ok(mcp.tools.some((tool) => tool.name === 'list_sandboxes'), 'MCP tools surfaced');
250
+ assert.ok(mcp.resource_templates.some((template) => template.uriTemplate === 'sandbox://sessions/{session_id}/screen'), 'MCP resource templates surfaced');
251
+ assert.equal((await f('/api/mcp', { method: 'POST', body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }) })).status, 503, 'MCP proxy fail-closed without token file');
252
+ const gatedReconnect = await f(`/api/instances/${DEFAULT_INSTANCE}/reconnect`, { method: 'POST' });
253
+ assert.equal(gatedReconnect.status, 409, 'Docker reconnect fallback is gated unless local dev flag is set');
254
+ assert.equal((await gatedReconnect.json()).error, 'local_docker_fallback_disabled', 'Docker reconnect fallback error is explicit');
205
255
 
206
- // approval inbox (UC-009)
207
- const pend = await (await f('/api/approvals?status=pending')).json();
208
- assert.equal(pend.derived, 'per-instance A2A input-required tasks', 'approvals derive from A2A tasks');
209
- assert.ok(pend.approvals.length >= 1, 'pending approvals seeded');
210
- const approvalId = pend.approvals[0].id;
211
- const apr = await (await f(`/api/approvals/${encodeURIComponent(approvalId)}?decision=approve`, { method: 'POST' })).json();
212
- assert.equal(apr.status.state, 'completed', 'approval response completes the task');
213
- const pend2 = await (await f('/api/approvals?status=pending')).json();
214
- assert.equal(pend2.approvals.length, pend.approvals.length - 1, 'approved item leaves the queue');
256
+ // Bridge registry HTTP/argv contract; actual CLI ranking/show integration remains separate.
257
+ const cap = await (await f("/api/capabilities?q=" + encodeURIComponent("deploy production") + "&limit=4")).json();
258
+ assert.ok(Array.isArray(cap.results) && cap.results.length >= 1, 'discover returns results');
259
+ const hit = cap.results.find((r) => r.name === 'flow-deploy-to-production');
260
+ assert.ok(hit, 'flow-deploy-to-production discoverable');
261
+ assert.ok(hit.name && hit.type, 'result carries name+type for show');
262
+ const shown = await (await f("/api/show?type=skill&name=flow-deploy-to-production")).json();
263
+ assert.match(shown.body, /name:\s*flow-deploy-to-production/, 'show returns the skill body');
264
+ assert.equal((await f("/api/capabilities")).status, 400, 'capabilities requires q');
265
+ // show by discovered PATH — deterministic, sidesteps ambiguous same-named artifacts (#1643)
266
+ if (hit.path) {
267
+ const shownByPath = await (await f(`/api/show?path=${encodeURIComponent(hit.path)}`)).json();
268
+ assert.match(shownByPath.body, /name:\s*flow-deploy-to-production/, 'show-by-path returns the body');
269
+ assert.equal(shownByPath.path, hit.path, 'show-by-path echoes the resolved path');
270
+ } else {
271
+ assert.ok(hit.id, 'pathless discover result carries a stable id');
272
+ }
273
+ // a missing artifact is a 4xx, never a 502 (ambiguous/not-found map to operator-correctable input)
274
+ assert.equal((await f('/api/show?type=agent&name=__definitely_not_a_real_artifact__')).status, 404, 'unknown artifact -> 404 not 502');
275
+ // a path outside the AIWG corpus is refused (no traversal)
276
+ assert.equal((await f(`/api/show?path=${encodeURIComponent('/etc/passwd')}`)).status, 400, 'path outside corpus -> 400');
215
277
 
216
- // cost rollup (UC-010)
217
- const cost = await (await f('/api/cost')).json();
218
- assert.ok(cost.total.usd > 0 && cost.per_instance.length >= 1, 'cost rollup present');
278
+ // contribution model: actions INJECT a command into a session — the Cockpit never
279
+ // runs the CLI (adr-cockpit-session-control-not-cli-runner) (#1591)
280
+ const contrib = await (await f("/api/contributions")).json();
281
+ assert.ok(contrib.sources.some((s) => s.id === 'aiwg-core'), 'aiwg-core contribution loaded');
282
+ const audit = contrib.actions.find((a) => a.id === 'audit-issues');
283
+ assert.ok(audit && typeof audit.inject.command === 'string', 'audit-issues declares an inject command');
284
+ assert.match(audit.inject.command, /issue-audit/, 'audit-issues injects the issue-audit command');
285
+ // the spawn-aiwg run endpoint is removed
286
+ assert.equal((await f("/api/actions/audit-issues/run", { method: 'POST' })).status, 404, 'action run endpoint gone (no Bridge CLI run for actions)');
219
287
 
220
- // destroy
221
- assert.equal((await (await f(`/api/instances/${stoppedId}`, { method: 'DELETE' })).json()).destroyed, stoppedId, 'destroy returns id');
288
+ // management: lifecycle (UC-012)
289
+ const stoppedId = '9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b';
290
+ assert.equal((await (await f(`/api/instances/${stoppedId}/start`, { method: 'POST' })).json()).state, 'running', 'start -> running');
291
+ assert.equal((await (await f(`/api/instances/${stoppedId}/stop`, { method: 'POST' })).json()).state, 'stopped', 'stop -> stopped');
222
292
 
223
- // user asset library: clone a catalog asset into the library, list it, delete it.
224
- // (AIWG source is read-only — clone copies into ~/.aiwg/cockpit/library, never the reverse.)
225
- const libraryPath = hit.path || fileURLToPath(new URL('../../../../agentic/code/frameworks/sdlc-complete/skills/flow-deploy-to-production/SKILL.md', import.meta.url));
226
- const cloneRes = await f(`/api/library/clone?type=${encodeURIComponent(hit.type)}&name=${encodeURIComponent(hit.name)}&path=${encodeURIComponent(libraryPath)}`, { method: 'POST' });
227
- assert.ok([201, 400].includes(cloneRes.status), 'clone returns 201 (new) or 400 (already present)');
228
- const lib1 = await (await f('/api/library')).json();
229
- assert.ok(lib1.library.some((a) => a.name === hit.name), 'cloned asset appears in the user library');
230
- assert.equal((await f(`/api/library/${encodeURIComponent(hit.name)}`, { method: 'DELETE' })).status, 200, 'library delete 200');
231
- const lib2 = await (await f('/api/library')).json();
232
- assert.ok(!lib2.library.some((a) => a.name === hit.name), 'deleted asset removed from library');
233
- // a path that escapes the library is refused
234
- assert.equal((await f('/api/library/..%2f..%2fevil', { method: 'DELETE' })).status, 404, 'escape attempt refused');
293
+ // management: cancel a running task
294
+ const before = await (await f('/api/running')).json();
295
+ const victim = before.running[0];
296
+ assert.equal((await f(`/api/tasks/${victim.instance_id}/${victim.task_id}/cancel`, { method: 'POST' })).status, 200, 'task cancel 200');
297
+ const after = await (await f('/api/running')).json();
298
+ assert.ok(after.count < before.count, 'cancel removed a running task');
235
299
 
236
- // start a session (onboarding primary verb): create + issue a ws attach_url
237
- const started = await (await f('/api/instances/550e8400-e29b-41d4-a716-446655440000/sessions', { method: 'POST' })).json();
238
- assert.match(started.id ?? '', /^sess-/, 'start-session returns a new session id');
239
- assert.match(started.attach_url ?? '', /\/sessions\/sess-[^/]+\/attach\/[A-Za-z0-9_-]+$/, 'start-session issues a proxied ws attach_url');
300
+ // approval inbox (UC-009)
301
+ const pend = await (await f('/api/approvals?status=pending')).json();
302
+ assert.equal(pend.derived, 'per-instance A2A input-required tasks', 'approvals derive from A2A tasks');
303
+ assert.ok(pend.approvals.length >= 1, 'pending approvals seeded');
304
+ const approvalId = pend.approvals[0].id;
305
+ const apr = await (await f(`/api/approvals/${encodeURIComponent(approvalId)}?decision=approve`, { method: 'POST' })).json();
306
+ assert.equal(apr.status.state, 'completed', 'approval response completes the task');
307
+ const pend2 = await (await f('/api/approvals?status=pending')).json();
308
+ assert.equal(pend2.approvals.length, pend.approvals.length - 1, 'approved item leaves the queue');
240
309
 
241
- // app shell is served without reusable credential material.
242
- const html = await (await fetch(base + "/")).text();
243
- assert.match(html, /AIWG.?Cockpit/i, 'app title rendered');
244
- assert.ok(!html.includes(bridge.cockpitToken), 'root does not inject the control token');
245
- assert.ok(!html.includes('__COCKPIT_TOKEN__'), 'root has no legacy token bootstrap global');
246
- // strip HTML comments BEFORE matching — a module script trapped inside a comment
247
- // (the Vite '</head>'-in-comment gotcha) must not count as "referenced".
248
- const live = html.replace(/<!--[\s\S]*?-->/g, '');
249
- const shell = /assets\//.test(html) ? 'react' : 'legacy';
250
- if (shell === 'react') {
251
- const asset = live.match(/<script[^>]+type="module"[^>]+src="([^"]*assets\/[^"]+\.js)"/);
252
- assert.ok(asset, 'React build present → module bundle must be referenced outside comments');
253
- assert.equal((await fetch(base + asset[1].replace(/^\.\//, '/'))).status, 200, 'built React bundle served');
254
- }
310
+ // cost rollup (UC-010)
311
+ const cost = await (await f('/api/cost')).json();
312
+ assert.ok(cost.total.usd > 0 && cost.per_instance.length >= 1, 'cost rollup present');
313
+
314
+ // destroy
315
+ assert.equal((await (await f(`/api/instances/${stoppedId}`, { method: 'DELETE' })).json()).destroyed, stoppedId, 'destroy returns id');
255
316
 
256
- console.log(`SMOKE OK — inventory(4) + running(${run.count}) + sessions(demo-shell) + mcp(${mcp.tools.length} tools) + registry(discover→${cap.results.length}) + contrib(${contrib.actions.length}) + shell(${shell})`);
257
- } finally {
258
- bridge.close();
259
- mock.close();
317
+ // Real clone/list/delete of an owned canonical-content fixture, with fresh ownership.
318
+ assert.equal(await readFile(skillPath, 'utf8'), shown.body, 'owned clone fixture preserves canonical shown skill bytes');
319
+ await exerciseLibraryRoundTrip({ f, sourcePath: skillPath, name: 'cockpit-smoke-' + randomUUID() });
320
+ // a path that escapes the library is refused
321
+ assert.equal((await f('/api/library/..%2f..%2fevil', { method: 'DELETE' })).status, 404, 'escape attempt refused');
322
+
323
+ // start a session (onboarding primary verb): create + issue a ws attach_url
324
+ const started = await (await f('/api/instances/550e8400-e29b-41d4-a716-446655440000/sessions', { method: 'POST' })).json();
325
+ assert.match(started.id ?? '', /^sess-/, 'start-session returns a new session id');
326
+ assert.match(started.attach_url ?? '', /\/sessions\/sess-[^/]+\/attach\/[A-Za-z0-9_-]+$/, 'start-session issues a proxied ws attach_url');
327
+
328
+ // app shell is served without reusable credential material.
329
+ const html = await (await fetch(base + "/")).text();
330
+ assert.match(html, /AIWG.?Cockpit/i, 'app title rendered');
331
+ assert.ok(!html.includes(bridge.cockpitToken), 'root does not inject the control token');
332
+ assert.ok(!html.includes('__COCKPIT_TOKEN__'), 'root has no legacy token bootstrap global');
333
+ // strip HTML comments BEFORE matching — a module script trapped inside a comment
334
+ // (the Vite '</head>'-in-comment gotcha) must not count as "referenced".
335
+ const live = html.replace(/<!--[\s\S]*?-->/g, '');
336
+ const shell = /assets\//.test(html) ? 'react' : 'legacy';
337
+ if (shell === 'react') {
338
+ const asset = live.match(/<script[^>]+type="module"[^>]+src="([^"]*assets\/[^"]+\.js)"/);
339
+ assert.ok(asset, 'React build present → module bundle must be referenced outside comments');
340
+ assert.equal((await fetch(base + asset[1].replace(/^\.\//, '/'))).status, 200, 'built React bundle served');
341
+ }
342
+
343
+ console.log(`SMOKE OK — inventory(4) + running(${run.count}) + sessions(demo-shell) + mcp(${mcp.tools.length} tools) + registry(CLI contract fixture→${cap.results.length}) + contrib(${contrib.actions.length}) + shell(${shell})`);
344
+ }, options);
260
345
  }
346
+
347
+ if (isDirectExecution(import.meta.url)) await runBridgeSmoke();