@aiwg/cockpit 2026.7.0 → 2026.7.3
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/bridge/src/server.mjs
CHANGED
|
@@ -31,6 +31,7 @@ const RUNTIME_DIR = join(homedir(), '.aiwg', 'cockpit', 'runtime');
|
|
|
31
31
|
// legacy vanilla page so the Bridge works even before a web build.
|
|
32
32
|
const WEB_DIST = fileURLToPath(new URL('../../web/dist', import.meta.url));
|
|
33
33
|
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.json': 'application/json', '.ico': 'image/x-icon', '.png': 'image/png', '.woff2': 'font/woff2', '.map': 'application/json' };
|
|
34
|
+
const CAPABILITY_TYPES = new Set(['skill', 'agent', 'command', 'rule', 'flow']);
|
|
34
35
|
|
|
35
36
|
/** Serve a static file from the built web app, sandboxed to WEB_DIST. Returns true if served. */
|
|
36
37
|
async function serveDistFile(res, relPath) {
|
|
@@ -294,6 +295,26 @@ async function probeExecutor(executorUrl) {
|
|
|
294
295
|
return false;
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
async function getExecutorCapabilities(executorUrl) {
|
|
299
|
+
const candidates = ['/healthz/deep', '/healthz', '/health'].map((path) => `${executorUrl}${path}`);
|
|
300
|
+
try {
|
|
301
|
+
const { target, body } = await fetchJsonFirst(candidates);
|
|
302
|
+
return {
|
|
303
|
+
status: 'ok',
|
|
304
|
+
source: new URL(target).pathname,
|
|
305
|
+
host_runtime_enabled: body.host_runtime_enabled === true || body.hostRuntimeEnabled === true,
|
|
306
|
+
raw_status: body.status ?? body.state ?? 'unknown',
|
|
307
|
+
};
|
|
308
|
+
} catch (err) {
|
|
309
|
+
return {
|
|
310
|
+
status: 'unreachable',
|
|
311
|
+
source: null,
|
|
312
|
+
host_runtime_enabled: false,
|
|
313
|
+
error: String(err?.message ?? err),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
297
318
|
function defaultExecutorCommand() {
|
|
298
319
|
if (EXECUTOR_COMMAND) return EXECUTOR_COMMAND.split(/\s+/).filter(Boolean);
|
|
299
320
|
const candidates = [
|
|
@@ -1074,6 +1095,7 @@ export function createBridge({ executorUrl = EXECUTOR_URL, allowMockExecutor = A
|
|
|
1074
1095
|
return;
|
|
1075
1096
|
}
|
|
1076
1097
|
if (url.pathname === '/api/inventory') return json(res, 200, await getInventory(upstreamUrl));
|
|
1098
|
+
if (url.pathname === '/api/executor/capabilities') return json(res, 200, await getExecutorCapabilities(upstreamUrl));
|
|
1077
1099
|
if (url.pathname === '/api/running') return json(res, 200, await getRunning(upstreamUrl));
|
|
1078
1100
|
if (url.pathname === '/api/loadouts') return json(res, 200, await getLoadouts(upstreamUrl));
|
|
1079
1101
|
let m;
|
|
@@ -1133,9 +1155,20 @@ export function createBridge({ executorUrl = EXECUTOR_URL, allowMockExecutor = A
|
|
|
1133
1155
|
if (url.pathname === '/api/capabilities') {
|
|
1134
1156
|
const q = (url.searchParams.get('q') || '').trim();
|
|
1135
1157
|
if (!q) return json(res, 400, { error: 'q_required' });
|
|
1136
|
-
const
|
|
1158
|
+
const rawLimit = url.searchParams.get('limit') ?? '8';
|
|
1159
|
+
const limit = Number(rawLimit);
|
|
1160
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
|
|
1161
|
+
return json(res, 400, { error: 'invalid_limit', detail: 'limit must be an integer from 1 to 50' });
|
|
1162
|
+
}
|
|
1163
|
+
const args = ['discover', q, '--json', '--limit', String(limit)];
|
|
1137
1164
|
const type = url.searchParams.get('type');
|
|
1138
|
-
if (type && type !== 'all')
|
|
1165
|
+
if (type && type !== 'all') {
|
|
1166
|
+
const types = type.split(',').map((t) => t.trim()).filter(Boolean);
|
|
1167
|
+
if (!types.length || types.some((t) => !CAPABILITY_TYPES.has(t))) {
|
|
1168
|
+
return json(res, 400, { error: 'invalid_type', detail: 'type must be all, skill, agent, command, rule, flow, or a comma list of those kinds' });
|
|
1169
|
+
}
|
|
1170
|
+
args.push('--type', types.join(','));
|
|
1171
|
+
}
|
|
1139
1172
|
const data = JSON.parse(await runAiwg(args));
|
|
1140
1173
|
data.results = (data.results || []).map((r) => ({ ...r, name: deriveName(r.path) }));
|
|
1141
1174
|
return json(res, 200, data);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cockpit",
|
|
3
|
-
"version": "2026.7.
|
|
3
|
+
"version": "2026.7.3",
|
|
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",
|
|
@@ -22,13 +22,22 @@ const LOADOUTS = [
|
|
|
22
22
|
{ id: 'agentic-dev', label: 'agentic-dev', description: 'Developer tools', runtimes: ['docker', 'container'] },
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
function mockFetch(
|
|
25
|
+
function mockFetch({
|
|
26
|
+
instances = [HOST_INSTANCE],
|
|
27
|
+
executorCaps = null,
|
|
28
|
+
createdInstanceId = 'docker-1',
|
|
29
|
+
}: {
|
|
30
|
+
instances?: unknown[];
|
|
31
|
+
executorCaps?: unknown;
|
|
32
|
+
createdInstanceId?: string;
|
|
33
|
+
} = {}) {
|
|
26
34
|
return vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
27
35
|
const url = String(input);
|
|
28
36
|
const ok = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
29
37
|
if (url.includes('/api/loadouts')) return ok({ loadouts: LOADOUTS });
|
|
30
|
-
if (url.includes('/api/inventory')) return ok({ instances
|
|
31
|
-
if (url.includes('/api/
|
|
38
|
+
if (url.includes('/api/inventory')) return ok({ instances });
|
|
39
|
+
if (url.includes('/api/executor/capabilities') && executorCaps) return ok(executorCaps);
|
|
40
|
+
if (url.includes('/api/instances') && init?.method === 'POST') return ok({ instance_id: createdInstanceId });
|
|
32
41
|
return new Response('{}', { status: 404 });
|
|
33
42
|
}) as unknown as typeof fetch;
|
|
34
43
|
}
|
|
@@ -37,7 +46,7 @@ beforeEach(() => { (window as unknown as { __COCKPIT_TOKEN__: string }).__COCKPI
|
|
|
37
46
|
afterEach(() => { cleanup(); vi.restoreAllMocks(); });
|
|
38
47
|
|
|
39
48
|
describe('LaunchInstanceModal', () => {
|
|
40
|
-
it('uses an existing host target
|
|
49
|
+
it('uses an existing host target when one is already registered', async () => {
|
|
41
50
|
globalThis.fetch = mockFetch();
|
|
42
51
|
const onLaunched = vi.fn();
|
|
43
52
|
const onClose = vi.fn();
|
|
@@ -56,6 +65,30 @@ describe('LaunchInstanceModal', () => {
|
|
|
56
65
|
expect(onClose).toHaveBeenCalled();
|
|
57
66
|
});
|
|
58
67
|
|
|
68
|
+
it('provisions the first host instance when the executor host supervisor is enabled', async () => {
|
|
69
|
+
globalThis.fetch = mockFetch({
|
|
70
|
+
instances: [],
|
|
71
|
+
executorCaps: { status: 'ok', source: '/healthz/deep', host_runtime_enabled: true },
|
|
72
|
+
createdInstanceId: 'host-created-1',
|
|
73
|
+
});
|
|
74
|
+
const onLaunched = vi.fn();
|
|
75
|
+
const onClose = vi.fn();
|
|
76
|
+
render(<LaunchInstanceModal open onClose={onClose} onLaunched={onLaunched} />);
|
|
77
|
+
|
|
78
|
+
expect(await screen.findByRole('option', { name: /local host runtime - create new/i })).toBeTruthy();
|
|
79
|
+
fireEvent.click(screen.getByRole('button', { name: /start host session/i }));
|
|
80
|
+
|
|
81
|
+
await waitFor(() => expect(onLaunched).toHaveBeenCalledWith('host-created-1', true, undefined));
|
|
82
|
+
const postCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls
|
|
83
|
+
.find((call) => String(call[0]).includes('/api/instances') && call[1]?.method === 'POST');
|
|
84
|
+
expect(JSON.parse(String(postCall?.[1]?.body))).toMatchObject({
|
|
85
|
+
runtime: 'host',
|
|
86
|
+
loadout: 'host-tools',
|
|
87
|
+
start: true,
|
|
88
|
+
});
|
|
89
|
+
expect(onClose).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
59
92
|
it('keeps Docker on the real provisioning path with Docker loadouts and images', async () => {
|
|
60
93
|
globalThis.fetch = mockFetch();
|
|
61
94
|
const onLaunched = vi.fn();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { api } from '../api';
|
|
3
3
|
import { fmtId } from '../util';
|
|
4
|
-
import type { Instance, Loadout } from '../types';
|
|
4
|
+
import type { ExecutorCapabilities, Instance, Loadout } from '../types';
|
|
5
5
|
|
|
6
6
|
type Runtime = 'host' | 'docker' | 'qemu';
|
|
7
7
|
|
|
@@ -46,6 +46,7 @@ export function LaunchInstanceModal({
|
|
|
46
46
|
const [loadouts, setLoadouts] = useState<Loadout[]>([]);
|
|
47
47
|
const [instances, setInstances] = useState<Instance[]>([]);
|
|
48
48
|
const [hostId, setHostId] = useState('');
|
|
49
|
+
const [executorCaps, setExecutorCaps] = useState<ExecutorCapabilities | null>(null);
|
|
49
50
|
const [image, setImage] = useState('agentic/codex:latest');
|
|
50
51
|
const [customImage, setCustomImage] = useState('');
|
|
51
52
|
const [profile, setProfile] = useState('');
|
|
@@ -65,11 +66,13 @@ export function LaunchInstanceModal({
|
|
|
65
66
|
Promise.all([
|
|
66
67
|
api<{ loadouts: Loadout[] }>('/api/loadouts').catch(() => ({ loadouts: [] as Loadout[] })),
|
|
67
68
|
api<{ instances: Instance[] }>('/api/inventory').catch(() => ({ instances: [] as Instance[] })),
|
|
69
|
+
api<ExecutorCapabilities>('/api/executor/capabilities').catch(() => null),
|
|
68
70
|
])
|
|
69
|
-
.then(([lo, inv]) => {
|
|
71
|
+
.then(([lo, inv, caps]) => {
|
|
70
72
|
if (cancelled) return;
|
|
71
73
|
setLoadouts(lo.loadouts ?? []);
|
|
72
74
|
setInstances(inv.instances ?? []);
|
|
75
|
+
setExecutorCaps(caps);
|
|
73
76
|
const firstHost = (inv.instances ?? []).find(isUsableHost);
|
|
74
77
|
setHostId((current) => current || firstHost?.id || '');
|
|
75
78
|
});
|
|
@@ -95,12 +98,36 @@ export function LaunchInstanceModal({
|
|
|
95
98
|
try {
|
|
96
99
|
if (runtime === 'host') {
|
|
97
100
|
const host = hostTargets(instances).find((i) => i.id === hostId) ?? hostTargets(instances)[0];
|
|
98
|
-
if (
|
|
101
|
+
if (host) {
|
|
102
|
+
setResult(openSession
|
|
103
|
+
? `Using host target ${host.launch_context?.name ?? fmtId(host.id)}; starting session...`
|
|
104
|
+
: `Using host target ${host.launch_context?.name ?? fmtId(host.id)}`);
|
|
105
|
+
await onLaunched(host.id, openSession);
|
|
106
|
+
if (openSession) onClose();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (!executorCaps?.host_runtime_enabled) {
|
|
110
|
+
throw new Error('Host runtime is not enabled on this executor. Enable the host supervisor or choose Docker container.');
|
|
111
|
+
}
|
|
112
|
+
const body = {
|
|
113
|
+
name: name.replace(/[^a-z0-9-]/g, '-').replace(/^-+/, 'a-').slice(0, 63),
|
|
114
|
+
runtime: 'host',
|
|
115
|
+
loadout: 'host-tools',
|
|
116
|
+
start: true,
|
|
117
|
+
};
|
|
118
|
+
const op = await api<{ id?: string; instance_id?: string; instanceId?: string; operation?: { id?: string }; result?: { instance_id?: string; instanceId?: string } }>('/api/instances', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'content-type': 'application/json' },
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
});
|
|
123
|
+
const instanceId = op.instance_id ?? op.instanceId ?? op.result?.instance_id ?? op.result?.instanceId;
|
|
124
|
+
const operationId = op.id ?? op.operation?.id;
|
|
99
125
|
setResult(openSession
|
|
100
|
-
? `
|
|
101
|
-
: `
|
|
102
|
-
await onLaunched(
|
|
126
|
+
? `Host launch accepted: ${instanceId ?? operationId ?? 'operation pending'}; waiting for session...`
|
|
127
|
+
: `Host launch accepted: ${instanceId ?? operationId ?? 'operation pending'}`);
|
|
128
|
+
await onLaunched(instanceId, openSession, operationId);
|
|
103
129
|
if (openSession) onClose();
|
|
130
|
+
else setName(genInstanceName());
|
|
104
131
|
return;
|
|
105
132
|
}
|
|
106
133
|
const body: Record<string, unknown> = {
|
|
@@ -155,8 +182,15 @@ export function LaunchInstanceModal({
|
|
|
155
182
|
<label htmlFor="li-host-target">Host target</label>
|
|
156
183
|
<select id="li-host-target" value={hostId} onChange={(e) => setHostId(e.target.value)}>
|
|
157
184
|
{hostTargets(instances).map((i) => <option key={i.id} value={i.id}>{i.launch_context?.name ?? fmtId(i.id)} - {i.loadout}</option>)}
|
|
158
|
-
{!hostTargets(instances).length && <option value="">
|
|
185
|
+
{!hostTargets(instances).length && executorCaps?.host_runtime_enabled && <option value="">Local host runtime - create new</option>}
|
|
186
|
+
{!hostTargets(instances).length && !executorCaps?.host_runtime_enabled && <option value="">Host runtime not enabled</option>}
|
|
159
187
|
</select>
|
|
188
|
+
{!hostTargets(instances).length && executorCaps?.host_runtime_enabled && (
|
|
189
|
+
<>
|
|
190
|
+
<label htmlFor="li-name">Name</label>
|
|
191
|
+
<input id="li-name" value={name} onChange={(e) => setName(e.target.value)} />
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
160
194
|
</>
|
|
161
195
|
) : (
|
|
162
196
|
<>
|
|
@@ -218,7 +252,7 @@ export function LaunchInstanceModal({
|
|
|
218
252
|
</div>
|
|
219
253
|
<div className="modal-actions">
|
|
220
254
|
<button onClick={onClose} disabled={busy}>Close</button>
|
|
221
|
-
<button className="cta" onClick={launch} disabled={busy || (runtime === 'host' && !hostId) || (runtime !== 'host' && !name.trim()) || (runtime === 'docker' && image === '__custom__' && !customImage.trim())}>
|
|
255
|
+
<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())}>
|
|
222
256
|
{busy ? 'Working...' : runtime === 'host' ? (openSession ? 'Start host session' : 'Use host') : openSession ? 'Create + start session' : 'Create instance'}
|
|
223
257
|
</button>
|
|
224
258
|
</div>
|
package/web/src/types.ts
CHANGED
|
@@ -41,6 +41,7 @@ export interface ResponseNeeded { id: string; instance_id: string; prompt: strin
|
|
|
41
41
|
export interface InstanceCost { instance_id: string; tenant: string; input_tokens: number; output_tokens: number; usd: number }
|
|
42
42
|
export interface Cost { total: { input_tokens: number; output_tokens: number; usd: number }; per_instance: InstanceCost[] }
|
|
43
43
|
export interface Loadout { id: string; label: string; description?: string; runtimes?: string[] }
|
|
44
|
+
export interface ExecutorCapabilities { status: string; source?: string | null; host_runtime_enabled: boolean; raw_status?: string; error?: string }
|
|
44
45
|
export interface CapabilityResult { path: string; type: string; title?: string; capability?: string; score?: number; name: string; triggers?: string[] }
|
|
45
46
|
export interface ContribAction { id: string; title: string; icon?: string; group?: string; source: string; inject: { command: string; target?: string; needs_args?: boolean; args_hint?: string } }
|
|
46
47
|
export type Role = 'controller' | 'observer' | null;
|