@aiwg/cockpit 2026.8.3 → 2026.8.5
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 +388 -18
- package/contrib/contribution.schema.json +2 -2
- package/package.json +1 -1
- package/shell-core/keychain.mjs +82 -63
- package/web/dist/assets/index-C5N6qGZf.js +312 -0
- package/web/dist/assets/{index-CQkRFleq.css → index-CLv_bmPX.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/App.test.tsx +19 -1
- package/web/src/App.tsx +4 -1
- package/web/src/api.ts +7 -1
- package/web/src/components/Actions.tsx +1 -0
- package/web/src/components/Activity.test.tsx +38 -0
- package/web/src/components/Activity.tsx +78 -0
- package/web/src/components/CapabilitySearch.tsx +5 -4
- package/web/src/components/Explore.tsx +45 -5
- package/web/src/components/Inventory.test.tsx +12 -0
- package/web/src/components/Inventory.tsx +7 -0
- package/web/src/components/LaunchInstanceModal.test.tsx +17 -0
- package/web/src/components/Missions.tsx +89 -4
- package/web/src/styles.css +8 -0
- package/web/src/types.ts +17 -3
- package/web/dist/assets/index-Dj3dFeFT.js +0 -312
package/bridge/src/server.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import http from 'node:http';
|
|
9
9
|
import https from 'node:https';
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import { readFile, mkdir, writeFile, chmod, readdir, cp, rm, stat, appendFile } from 'node:fs/promises';
|
|
11
|
+
import { readFile, mkdir, writeFile, rename, chmod, readdir, cp, rm, stat, appendFile } from 'node:fs/promises';
|
|
12
12
|
import { existsSync, realpathSync } from 'node:fs';
|
|
13
13
|
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
14
14
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -42,7 +42,13 @@ const auditLog = () => join(auditDir(), 'events.jsonl');
|
|
|
42
42
|
// legacy vanilla page so the Bridge works even before a web build.
|
|
43
43
|
const WEB_DIST = fileURLToPath(new URL('../../web/dist', import.meta.url));
|
|
44
44
|
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' };
|
|
45
|
-
|
|
45
|
+
// Discovery indexes more than the four executable provider artifacts. Keep the
|
|
46
|
+
// Bridge filter aligned with the complete corpus so Explore does not hide
|
|
47
|
+
// extension and documentation surfaces (#1592).
|
|
48
|
+
const CAPABILITY_TYPES = new Set([
|
|
49
|
+
'skill', 'agent', 'command', 'rule', 'flow', 'behavior', 'hook', 'template',
|
|
50
|
+
'tool', 'addon', 'framework', 'extension', 'plugin', 'provider', 'document',
|
|
51
|
+
]);
|
|
46
52
|
const mcSessionsDir = () => join(process.cwd(), '.aiwg', 'ralph-external', 'mc', 'sessions');
|
|
47
53
|
const executorRequestContext = new AsyncLocalStorage();
|
|
48
54
|
|
|
@@ -177,13 +183,88 @@ function spawnCollect(cmd, args) {
|
|
|
177
183
|
p.stdout.on('data', (d) => (out += d));
|
|
178
184
|
p.stderr.on('data', (d) => (err += d));
|
|
179
185
|
p.once('error', reject);
|
|
180
|
-
p.once('close', (code) =>
|
|
186
|
+
p.once('close', (code) => {
|
|
187
|
+
if (code === 0) return resolve(out);
|
|
188
|
+
const failure = new Error(err.trim() || `aiwg exit ${code}`);
|
|
189
|
+
failure.exitCode = code;
|
|
190
|
+
failure.stdout = out;
|
|
191
|
+
reject(failure);
|
|
192
|
+
});
|
|
181
193
|
});
|
|
182
194
|
}
|
|
183
195
|
async function runAiwg(args) {
|
|
184
196
|
try { return await spawnCollect('aiwg', args); }
|
|
185
197
|
catch (e) { if (e && e.code === 'ENOENT') return spawnCollect(process.execPath, [REPO_BIN, ...args]); throw e; }
|
|
186
198
|
}
|
|
199
|
+
|
|
200
|
+
const MISSION_CONTROL_ID_RE = /^[a-zA-Z0-9._-]+$/;
|
|
201
|
+
async function controlMission({ action, sessionId, missionId, expectedUpdatedAt, requestId }) {
|
|
202
|
+
if (!['pause', 'resume', 'cancel'].includes(action)) throw Object.assign(new Error('unsupported mission control action'), { status: 400 });
|
|
203
|
+
if (!MISSION_CONTROL_ID_RE.test(sessionId) || (missionId && !MISSION_CONTROL_ID_RE.test(missionId))) {
|
|
204
|
+
throw Object.assign(new Error('invalid Mission control identifier'), { status: 400 });
|
|
205
|
+
}
|
|
206
|
+
const args = ['mc', action, sessionId];
|
|
207
|
+
if (action === 'cancel') {
|
|
208
|
+
if (!missionId) throw Object.assign(new Error('mission id required'), { status: 400 });
|
|
209
|
+
args.push(missionId);
|
|
210
|
+
}
|
|
211
|
+
if (expectedUpdatedAt) args.push('--expected-updated-at', String(expectedUpdatedAt));
|
|
212
|
+
if (requestId) args.push('--request-id', String(requestId));
|
|
213
|
+
await appendAudit('mission.control.requested', {
|
|
214
|
+
action,
|
|
215
|
+
session_id: sessionId,
|
|
216
|
+
mission_id: missionId ?? null,
|
|
217
|
+
expected_updated_at: expectedUpdatedAt ?? null,
|
|
218
|
+
request_id: requestId ?? null,
|
|
219
|
+
});
|
|
220
|
+
try {
|
|
221
|
+
await runAiwg(args);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const message = String(error?.message ?? error);
|
|
224
|
+
const status = error?.exitCode === 3 || /mission_conflict/.test(message) ? 409 : 422;
|
|
225
|
+
await appendAudit('mission.control.rejected', { action, session_id: sessionId, mission_id: missionId ?? null, request_id: requestId ?? null, status, reason: message });
|
|
226
|
+
throw Object.assign(new Error(message), { status });
|
|
227
|
+
}
|
|
228
|
+
await appendAudit('mission.control.completed', { action, session_id: sessionId, mission_id: missionId ?? null, request_id: requestId ?? null });
|
|
229
|
+
return { ok: true, action, session_id: sessionId, mission_id: missionId ?? null, request_id: requestId ?? null };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function dispatchMission(body, upstreamUrl) {
|
|
233
|
+
const sessionId = String(body?.session_id ?? '');
|
|
234
|
+
const objective = String(body?.objective ?? '').trim();
|
|
235
|
+
const completion = String(body?.completion ?? '').trim();
|
|
236
|
+
const requestId = String(body?.request_id ?? randomBytes(16).toString('hex'));
|
|
237
|
+
if (!MISSION_CONTROL_ID_RE.test(sessionId)) throw Object.assign(new Error('invalid Mission control session id'), { status: 400 });
|
|
238
|
+
if (!objective || objective.length > 4096) throw Object.assign(new Error('objective is required and must be at most 4096 characters'), { status: 400 });
|
|
239
|
+
if (completion.length > 4096) throw Object.assign(new Error('completion must be at most 4096 characters'), { status: 400 });
|
|
240
|
+
if (!MISSION_CONTROL_ID_RE.test(requestId)) throw Object.assign(new Error('request_id must contain only letters, digits, dot, underscore, or hyphen'), { status: 400 });
|
|
241
|
+
const args = ['mc', 'dispatch', sessionId, objective, '--request-id', requestId];
|
|
242
|
+
if (completion) args.push('--completion', completion);
|
|
243
|
+
if (body?.priority) args.push('--priority', String(body.priority));
|
|
244
|
+
if (body?.expected_updated_at) args.push('--expected-updated-at', String(body.expected_updated_at));
|
|
245
|
+
if (body?.max_iterations !== undefined) {
|
|
246
|
+
const maxIterations = Number(body.max_iterations);
|
|
247
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1 || maxIterations > 10_000) {
|
|
248
|
+
throw Object.assign(new Error('max_iterations must be an integer from 1 to 10000'), { status: 400 });
|
|
249
|
+
}
|
|
250
|
+
args.push('--max-iterations', String(maxIterations));
|
|
251
|
+
}
|
|
252
|
+
await appendAudit('mission.dispatch.requested', { session_id: sessionId, request_id: requestId, objective_digest: `sha256:${createHash('sha256').update(objective).digest('hex')}` });
|
|
253
|
+
try {
|
|
254
|
+
await runAiwg(args);
|
|
255
|
+
if (body?.run === true) {
|
|
256
|
+
await runAiwg(['mc', 'run', sessionId, ...(body?.accept_cost === true ? ['--accept-cost'] : [])]);
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const message = String(error?.message ?? error);
|
|
260
|
+
const status = error?.exitCode === 3 || /mission_conflict/.test(message) ? 409 : 422;
|
|
261
|
+
await appendAudit('mission.dispatch.rejected', { session_id: sessionId, request_id: requestId, status, reason: message });
|
|
262
|
+
throw Object.assign(new Error(message), { status });
|
|
263
|
+
}
|
|
264
|
+
const missionId = `m-${createHash('sha256').update(requestId).digest('hex').slice(0, 16)}`;
|
|
265
|
+
await appendAudit('mission.dispatch.completed', { session_id: sessionId, mission_id: missionId, request_id: requestId, run: body?.run === true });
|
|
266
|
+
return { ok: true, session_id: sessionId, mission_id: missionId, request_id: requestId, projection: await getMissions(upstreamUrl) };
|
|
267
|
+
}
|
|
187
268
|
// --- user asset library (#1591/#1593): the operator's OWN copied/cloned/imported
|
|
188
269
|
// assets, on disk under ~/.aiwg/cockpit/library. AIWG install files are NEVER written
|
|
189
270
|
// (clone reads the catalog read-only, writes only into the library). ---
|
|
@@ -256,23 +337,32 @@ function resolveCorpusPath(p) {
|
|
|
256
337
|
// --- UI contribution model (#1591): declarative screens/actions/event-hooks ---
|
|
257
338
|
const ID_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
258
339
|
/** Validate one contribution manifest. Throws with a precise message on bad shape. */
|
|
259
|
-
function validateContribution(m, where) {
|
|
340
|
+
function validateContribution(m, where, { firstParty = false } = {}) {
|
|
260
341
|
const fail = (msg) => { throw new Error(`${where}: ${msg}`); };
|
|
261
342
|
if (!m || typeof m !== 'object') fail('manifest must be an object');
|
|
262
343
|
if (!ID_RE.test(m.id || '')) fail('id must match [a-z0-9._-]{1,64}');
|
|
263
|
-
if (typeof m.version !== 'string') fail('version
|
|
344
|
+
if (typeof m.version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(m.version)) fail('version must be semantic version syntax');
|
|
264
345
|
const c = m.contributes || {};
|
|
346
|
+
const actionIds = new Set();
|
|
265
347
|
for (const a of c.actions || []) {
|
|
266
348
|
if (!ID_RE.test(a.id || '')) fail(`action.id invalid: ${a.id}`);
|
|
349
|
+
if (actionIds.has(a.id)) fail(`duplicate action.id: ${a.id}`);
|
|
350
|
+
actionIds.add(a.id);
|
|
267
351
|
if (typeof a.title !== 'string') fail(`action ${a.id}: title required`);
|
|
268
352
|
// An action INJECTS a command into an agentic session — it does NOT run the CLI.
|
|
269
353
|
if (!a.inject || typeof a.inject.command !== 'string') fail(`action ${a.id}: inject.command (string) required`);
|
|
354
|
+
if (!/^\/[a-z0-9][a-z0-9._-]*(?:\s[^\r\n]*)?$/i.test(a.inject.command)) fail(`action ${a.id}: inject.command must be one slash command without newlines`);
|
|
270
355
|
if (a.inject.target && !['focused', 'new'].includes(a.inject.target)) fail(`action ${a.id}: inject.target must be focused|new`);
|
|
271
356
|
}
|
|
272
357
|
for (const s of c.screens || []) {
|
|
273
358
|
if (!ID_RE.test(s.id || '')) fail(`screen.id invalid: ${s.id}`);
|
|
274
359
|
if (typeof s.title !== 'string') fail(`screen ${s.id}: title required`);
|
|
275
360
|
if (typeof s.source !== 'string') fail(`screen ${s.id}: source required`);
|
|
361
|
+
if (firstParty) {
|
|
362
|
+
if (!s.source.startsWith('cockpit://')) fail(`screen ${s.id}: first-party source must use cockpit://`);
|
|
363
|
+
} else if (!s.source.startsWith(`sandbox://${m.id}/`)) {
|
|
364
|
+
fail(`screen ${s.id}: third-party source must use sandbox://${m.id}/`);
|
|
365
|
+
}
|
|
276
366
|
}
|
|
277
367
|
for (const w of c.workflows || []) {
|
|
278
368
|
if (!ID_RE.test(w.id || '')) fail(`workflow.id invalid: ${w.id}`);
|
|
@@ -280,24 +370,39 @@ function validateContribution(m, where) {
|
|
|
280
370
|
if (!Array.isArray(w.steps) || w.steps.length === 0) fail(`workflow ${w.id}: steps required`);
|
|
281
371
|
for (const step of w.steps) {
|
|
282
372
|
if (!step || typeof step !== 'object' || !ID_RE.test(step.action || '')) fail(`workflow ${w.id}: step.action invalid`);
|
|
373
|
+
if (!actionIds.has(step.action)) fail(`workflow ${w.id}: unknown action ${step.action}`);
|
|
283
374
|
}
|
|
284
375
|
}
|
|
285
|
-
for (const h of c.hooks || []) {
|
|
376
|
+
for (const h of c.hooks || []) {
|
|
377
|
+
if (typeof h.on !== 'string' || !ID_RE.test(h.action || '') || !actionIds.has(h.action)) fail(`hook invalid: on=${h.on}`);
|
|
378
|
+
}
|
|
286
379
|
return m;
|
|
287
380
|
}
|
|
288
381
|
/** Load + validate + merge all contribution manifests across the configured dirs. */
|
|
289
382
|
async function loadContributions() {
|
|
290
383
|
const sources = [], actions = [], screens = [], hooks = [], workflows = [];
|
|
291
|
-
|
|
384
|
+
const manifestIds = new Set();
|
|
385
|
+
const itemIds = new Set();
|
|
386
|
+
for (const [dirIndex, dir] of CONTRIB_DIRS.entries()) {
|
|
387
|
+
const trustTier = dirIndex === 0 ? 'first-party' : 'sandboxed-third-party';
|
|
292
388
|
let entries = [];
|
|
293
389
|
try { entries = (await readdir(dir)).filter((f) => f.endsWith('.json') && f !== 'contribution.schema.json'); } catch { continue; }
|
|
294
390
|
for (const file of entries) {
|
|
295
|
-
const m = validateContribution(JSON.parse(await readFile(join(dir, file), 'utf8')), file);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
for (const
|
|
300
|
-
|
|
391
|
+
const m = validateContribution(JSON.parse(await readFile(join(dir, file), 'utf8')), file, { firstParty: dirIndex === 0 });
|
|
392
|
+
if (manifestIds.has(m.id)) throw new Error(`${file}: duplicate contribution id ${m.id}`);
|
|
393
|
+
manifestIds.add(m.id);
|
|
394
|
+
sources.push({ id: m.id, version: m.version, title: m.title ?? m.id, file, trust_tier: trustTier });
|
|
395
|
+
for (const [kind, rows] of Object.entries({ actions: m.contributes?.actions || [], screens: m.contributes?.screens || [], hooks: m.contributes?.hooks || [], workflows: m.contributes?.workflows || [] })) {
|
|
396
|
+
for (const row of rows) {
|
|
397
|
+
const globalId = `${kind}:${row.id ?? `${row.on}:${row.action}`}`;
|
|
398
|
+
if (itemIds.has(globalId)) throw new Error(`${file}: duplicate ${globalId}`);
|
|
399
|
+
itemIds.add(globalId);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const a of m.contributes?.actions || []) actions.push({ ...a, source: m.id, trust_tier: trustTier });
|
|
403
|
+
for (const s of m.contributes?.screens || []) screens.push({ ...s, contribution: m.id, trust_tier: trustTier });
|
|
404
|
+
for (const h of m.contributes?.hooks || []) hooks.push({ ...h, source: m.id, trust_tier: trustTier });
|
|
405
|
+
for (const w of m.contributes?.workflows || []) workflows.push({ ...w, source: m.id, trust_tier: trustTier });
|
|
301
406
|
}
|
|
302
407
|
}
|
|
303
408
|
return { sources, actions, screens, hooks, workflows };
|
|
@@ -356,6 +461,42 @@ async function rebuildIndex(req) {
|
|
|
356
461
|
return { status: 200, body: { ok: true, command: `aiwg ${args.join(' ')}`, output, status } };
|
|
357
462
|
}
|
|
358
463
|
|
|
464
|
+
export async function createUserIndexGraph(body, projectRoot = process.cwd()) {
|
|
465
|
+
const name = safeIndexGraph(body?.name);
|
|
466
|
+
if (!name || ['project', 'codebase', 'framework'].includes(name)) {
|
|
467
|
+
throw new Error('name must be a non-built-in graph identifier');
|
|
468
|
+
}
|
|
469
|
+
const scanDirs = Array.isArray(body?.scanDirs) ? body.scanDirs.map((value) => String(value).trim()) : [];
|
|
470
|
+
if (!scanDirs.length || scanDirs.some((value) => !value || value.startsWith('/') || value.split(/[\\/]+/).includes('..'))) {
|
|
471
|
+
throw new Error('scanDirs must contain safe project-relative paths');
|
|
472
|
+
}
|
|
473
|
+
const extensions = Array.isArray(body?.extensions) && body.extensions.length
|
|
474
|
+
? body.extensions.map((value) => String(value).trim())
|
|
475
|
+
: ['.md', '.yaml', '.json'];
|
|
476
|
+
if (extensions.some((value) => !/^\.[a-z0-9]+$/i.test(value))) {
|
|
477
|
+
throw new Error('extensions must use forms such as .md or .json');
|
|
478
|
+
}
|
|
479
|
+
const configDir = join(projectRoot, '.aiwg');
|
|
480
|
+
const configPath = join(configDir, 'aiwg.config');
|
|
481
|
+
await mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
482
|
+
let config = {};
|
|
483
|
+
try { config = JSON.parse(await readFile(configPath, 'utf8')); }
|
|
484
|
+
catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
485
|
+
config.index = config.index && typeof config.index === 'object' ? config.index : {};
|
|
486
|
+
config.index.graphs = config.index.graphs && typeof config.index.graphs === 'object' ? config.index.graphs : {};
|
|
487
|
+
if (config.index.graphs[name]) throw new Error(`graph '${name}' already exists`);
|
|
488
|
+
config.index.graphs[name] = {
|
|
489
|
+
scanDirs,
|
|
490
|
+
extensions,
|
|
491
|
+
defaultBuild: body?.defaultBuild === true,
|
|
492
|
+
shared: body?.shared === true,
|
|
493
|
+
};
|
|
494
|
+
const temporary = `${configPath}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
|
|
495
|
+
await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
496
|
+
await rename(temporary, configPath);
|
|
497
|
+
return { name, definition: config.index.graphs[name], config_path: configPath };
|
|
498
|
+
}
|
|
499
|
+
|
|
359
500
|
function json(res, status, body) {
|
|
360
501
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
361
502
|
res.end(JSON.stringify(body));
|
|
@@ -1392,6 +1533,7 @@ function normalizeInstance(executorUrl, i) {
|
|
|
1392
1533
|
? { mode: i.transport, trust: i.transport_posture, source: 'agentic-sandbox admin-v2' }
|
|
1393
1534
|
: i.transport ?? i.transport_posture ?? i.security_posture ?? i.security?.transport,
|
|
1394
1535
|
),
|
|
1536
|
+
managed_docker_posture: normalizeManagedDockerPosture(i, runtimePosture.kind),
|
|
1395
1537
|
launch_context: {
|
|
1396
1538
|
cwd: i.launch_context?.cwd ?? i.launchContext?.cwd ?? i.cwd,
|
|
1397
1539
|
loadout,
|
|
@@ -1410,6 +1552,154 @@ function normalizeInstance(executorUrl, i) {
|
|
|
1410
1552
|
};
|
|
1411
1553
|
}
|
|
1412
1554
|
|
|
1555
|
+
const MANAGED_DOCKER_CONTROL_UID_MIN = 200_000;
|
|
1556
|
+
const MANAGED_DOCKER_CONTROL_UID_MAX = 799_999;
|
|
1557
|
+
const MANAGED_DOCKER_WORKLOAD_UID = 10_001;
|
|
1558
|
+
|
|
1559
|
+
/** Project only executor-attested, client-safe managed-Docker identity evidence. */
|
|
1560
|
+
export function normalizeManagedDockerPosture(i, runtimeKind) {
|
|
1561
|
+
if (!['docker', 'container'].includes(String(runtimeKind).toLowerCase())) return undefined;
|
|
1562
|
+
const source = i.managed_docker_posture ?? i.managedDockerPosture ?? i.security_posture ?? i.securityPosture ?? i;
|
|
1563
|
+
const rawTransport = source.transport_mode ?? source.transportMode
|
|
1564
|
+
?? (typeof source.transport === 'string' ? source.transport : source.transport?.mode)
|
|
1565
|
+
?? (typeof i.transport === 'string' ? i.transport : i.transport?.mode)
|
|
1566
|
+
?? 'unknown';
|
|
1567
|
+
const transportMode = String(rawTransport).toLowerCase();
|
|
1568
|
+
const rawControlUid = source.control_uid ?? source.controlUid;
|
|
1569
|
+
const controlUid = Number.isInteger(Number(rawControlUid)) ? Number(rawControlUid) : undefined;
|
|
1570
|
+
const rawWorkloadUid = source.workload_uid ?? source.workloadUid;
|
|
1571
|
+
const workloadUid = Number.isInteger(Number(rawWorkloadUid)) ? Number(rawWorkloadUid) : undefined;
|
|
1572
|
+
const boundary = String(source.workload_boundary ?? source.workloadBoundary ?? source.boundary ?? 'unknown').toLowerCase();
|
|
1573
|
+
const reportedFallback = String(source.fallback_reason_code ?? source.fallbackReasonCode ?? source.fallback_reason ?? source.fallbackReason ?? '').toLowerCase();
|
|
1574
|
+
const fallbackReason = transportMode === 'mtls-bootstrap' || reportedFallback === 'docker_desktop_peer_uid_unavailable'
|
|
1575
|
+
? 'Docker Desktop UDS bridge does not preserve peer UID'
|
|
1576
|
+
: reportedFallback === 'identity_resolver_unavailable'
|
|
1577
|
+
? 'Managed UDS identity resolver unavailable'
|
|
1578
|
+
: ['operator-configured', 'explicit', 'mtls'].includes(transportMode)
|
|
1579
|
+
? 'Operator-configured compatibility transport'
|
|
1580
|
+
: undefined;
|
|
1581
|
+
const controlIdentityPresent = controlUid !== undefined;
|
|
1582
|
+
const controlIdentityRangeValid = controlIdentityPresent
|
|
1583
|
+
&& controlUid >= MANAGED_DOCKER_CONTROL_UID_MIN
|
|
1584
|
+
&& controlUid <= MANAGED_DOCKER_CONTROL_UID_MAX;
|
|
1585
|
+
const workloadIdentitySeparated = boundary === 'separated' && workloadUid === MANAGED_DOCKER_WORKLOAD_UID;
|
|
1586
|
+
const secureDefault = transportMode === 'uds' && controlIdentityRangeValid && workloadIdentitySeparated;
|
|
1587
|
+
const compatibility = transportMode !== 'uds';
|
|
1588
|
+
const requiresRecreation = !controlIdentityPresent || !workloadUid || boundary === 'unknown';
|
|
1589
|
+
return {
|
|
1590
|
+
transport_mode: transportMode,
|
|
1591
|
+
control_identity_present: controlIdentityPresent,
|
|
1592
|
+
control_identity_range_valid: controlIdentityRangeValid,
|
|
1593
|
+
workload_uid: workloadUid,
|
|
1594
|
+
workload_identity_separated: workloadIdentitySeparated,
|
|
1595
|
+
boundary,
|
|
1596
|
+
secure_default: secureDefault,
|
|
1597
|
+
compatibility,
|
|
1598
|
+
fallback_reason: fallbackReason ? String(fallbackReason).slice(0, 300) : undefined,
|
|
1599
|
+
requires_recreation: requiresRecreation,
|
|
1600
|
+
source: 'agentic-sandbox',
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const ACTIVITY_SCOPE_HEADERS = {
|
|
1605
|
+
tenant_id: 'x-agentic-tenant-id', host_id: 'x-agentic-host-id',
|
|
1606
|
+
instance_id: 'x-agentic-instance-id', agent_id: 'x-agentic-agent-id',
|
|
1607
|
+
};
|
|
1608
|
+
const ACTIVITY_FILTERS = new Set(['event_name', 'collector', 'trust', 'plane', 'outcome', 'session_id', 'mission_id', 'task_id', 'tool_call_id', 'command_id', 'process_id', 'trace_id', 'since', 'until', 'limit']);
|
|
1609
|
+
const RESTRICTED_ACTIVITY_KEY = /(?:^|_)(?:content|terminal|prompt|environment|env|credential|secret|password|authorization|bearer|token|private_key|certificate|restricted_(?:url|uri|link))(?:$|_)/i;
|
|
1610
|
+
|
|
1611
|
+
export function activityRequest(input = {}) {
|
|
1612
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw Object.assign(new Error('activity request must be an object'), { code: 'activity_invalid_request' });
|
|
1613
|
+
const headers = { 'accept': 'application/json' };
|
|
1614
|
+
const scope = {};
|
|
1615
|
+
for (const [key, header] of Object.entries(ACTIVITY_SCOPE_HEADERS)) {
|
|
1616
|
+
const value = String(input[key] ?? '').trim();
|
|
1617
|
+
if (!value || value.length > 255 || /[\r\n]/.test(value)) throw Object.assign(new Error(`missing or invalid ${key}`), { code: 'activity_scope_required' });
|
|
1618
|
+
headers[header] = value;
|
|
1619
|
+
scope[key] = value;
|
|
1620
|
+
}
|
|
1621
|
+
const filter = {};
|
|
1622
|
+
for (const [key, value] of Object.entries(input.filter ?? {})) {
|
|
1623
|
+
if (!ACTIVITY_FILTERS.has(key)) throw Object.assign(new Error(`unsupported activity filter: ${key}`), { code: 'activity_invalid_filter' });
|
|
1624
|
+
if (key === 'limit') {
|
|
1625
|
+
if (!Number.isInteger(value) || value < 1 || value > 1000) throw Object.assign(new Error('activity limit must be 1..1000'), { code: 'activity_invalid_filter' });
|
|
1626
|
+
filter[key] = value;
|
|
1627
|
+
} else if (typeof value === 'string' && value.trim() && value.length <= 255 && !/[\r\n]/.test(value)) filter[key] = value.trim();
|
|
1628
|
+
else throw Object.assign(new Error(`invalid activity filter: ${key}`), { code: 'activity_invalid_filter' });
|
|
1629
|
+
}
|
|
1630
|
+
return { headers, scope, filter };
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function hasRestrictedActivityField(value) {
|
|
1634
|
+
if (Array.isArray(value)) return value.some(hasRestrictedActivityField);
|
|
1635
|
+
if (!value || typeof value !== 'object') return false;
|
|
1636
|
+
return Object.entries(value).some(([key, child]) => RESTRICTED_ACTIVITY_KEY.test(key) || hasRestrictedActivityField(child));
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
export function validateActivityEnvelope(body, expectedScope, { includeEvents = false, exportEnvelope = false } = {}) {
|
|
1640
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) throw Object.assign(new Error('malformed activity envelope'), { code: 'activity_malformed_envelope' });
|
|
1641
|
+
if (!exportEnvelope && body.schema_version !== 'activity.event/v1') throw Object.assign(new Error('unsupported activity schema'), { code: 'activity_malformed_envelope' });
|
|
1642
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
1643
|
+
if (includeEvents && !Array.isArray(body.events)) throw Object.assign(new Error('activity envelope has no events array'), { code: 'activity_malformed_envelope' });
|
|
1644
|
+
if (!exportEnvelope && (!Array.isArray(body.coverage) || !body.completeness || typeof body.completeness.complete !== 'boolean')) {
|
|
1645
|
+
throw Object.assign(new Error('activity envelope has invalid coverage'), { code: 'activity_malformed_envelope' });
|
|
1646
|
+
}
|
|
1647
|
+
const nonnegativeInteger = (value) => Number.isInteger(value) && value >= 0;
|
|
1648
|
+
const nonnegativeFinite = (value) => Number.isFinite(value) && value >= 0;
|
|
1649
|
+
const validCompleteness = (value) => value
|
|
1650
|
+
&& typeof value.label === 'string'
|
|
1651
|
+
&& nonnegativeInteger(value.collector_count)
|
|
1652
|
+
&& nonnegativeInteger(value.sequence_gap_count)
|
|
1653
|
+
&& nonnegativeInteger(value.durable_loss_count)
|
|
1654
|
+
&& nonnegativeInteger(value.restart_count)
|
|
1655
|
+
&& nonnegativeInteger(value.dropped_event_count)
|
|
1656
|
+
&& nonnegativeInteger(value.stale_collector_count)
|
|
1657
|
+
&& Array.isArray(value.unsupported_event_classes)
|
|
1658
|
+
&& value.unsupported_event_classes.every((item) => typeof item === 'string')
|
|
1659
|
+
&& nonnegativeFinite(value.maximum_clock_error_ms);
|
|
1660
|
+
if (!exportEnvelope && !validCompleteness(body.completeness)) {
|
|
1661
|
+
throw Object.assign(new Error('activity envelope has malformed completeness summary'), { code: 'activity_malformed_envelope' });
|
|
1662
|
+
}
|
|
1663
|
+
if (!exportEnvelope && body.coverage.some((entry) => !entry || typeof entry.collector_id !== 'string' || !Array.isArray(entry.sequence_gaps) || !Array.isArray(entry.durable_loss_records) || !nonnegativeInteger(entry.restart_count) || !nonnegativeInteger(entry.dropped_event_count) || typeof entry.stale !== 'boolean' || !Array.isArray(entry.unsupported_event_classes) || !entry.unsupported_event_classes.every((item) => typeof item === 'string') || !nonnegativeFinite(entry.maximum_clock_error_ms))) {
|
|
1664
|
+
throw Object.assign(new Error('activity envelope has malformed collector coverage'), { code: 'activity_malformed_envelope' });
|
|
1665
|
+
}
|
|
1666
|
+
for (const event of events) {
|
|
1667
|
+
if (event?.schema_version !== 'activity.event/v1' || event?.sensitivity !== 'metadata' || hasRestrictedActivityField(event)) {
|
|
1668
|
+
throw Object.assign(new Error('activity envelope contains restricted or unsupported event data'), { code: 'activity_restricted_data' });
|
|
1669
|
+
}
|
|
1670
|
+
for (const [key, value] of Object.entries(expectedScope)) {
|
|
1671
|
+
if (event?.correlation?.[key] !== value) throw Object.assign(new Error('activity event scope mismatch'), { code: 'activity_scope_mismatch' });
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (exportEnvelope && (!body.manifest || typeof body.manifest.key_id !== 'string' || typeof body.manifest.merkle_root !== 'string')) {
|
|
1675
|
+
throw Object.assign(new Error('signed activity export has no valid manifest'), { code: 'activity_malformed_export' });
|
|
1676
|
+
}
|
|
1677
|
+
return body;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async function activityProxy(executorUrl, kind, input) {
|
|
1681
|
+
const request = activityRequest(input);
|
|
1682
|
+
const isExport = kind === 'export';
|
|
1683
|
+
const query = new URLSearchParams(Object.entries(request.filter).map(([key, value]) => [key, String(value)]));
|
|
1684
|
+
const target = `${executorUrl}/api/v2/activity/${kind}${!isExport && query.size ? `?${query}` : ''}`;
|
|
1685
|
+
const result = await fetchJsonFirst([{ target, method: isExport ? 'POST' : 'GET', headers: { ...request.headers, ...(isExport ? { 'content-type': 'application/json' } : {}) }, body: isExport ? JSON.stringify(request.filter) : undefined }]);
|
|
1686
|
+
if (!result.status.toString().startsWith('2')) return result;
|
|
1687
|
+
return { ...result, body: validateActivityEnvelope(result.body, request.scope, { includeEvents: kind === 'timeline' || isExport, exportEnvelope: isExport }) };
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function managedDockerLaunchError(status, body) {
|
|
1691
|
+
const detail = String(body?.message ?? body?.error?.message ?? body?.error ?? body?.failure?.message ?? '');
|
|
1692
|
+
if (/refuses startup profiles that materialize raw credential refs/i.test(detail)) return {
|
|
1693
|
+
status: status >= 400 ? status : 422,
|
|
1694
|
+
body: {
|
|
1695
|
+
error: 'managed_docker_raw_credentials_rejected',
|
|
1696
|
+
message: 'Managed Docker does not accept startup profiles with raw credential references.',
|
|
1697
|
+
recovery: 'Use the sandbox credential proxy or select a VM runtime. Cockpit will not downgrade the transport automatically.',
|
|
1698
|
+
},
|
|
1699
|
+
};
|
|
1700
|
+
return { status, body };
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1413
1703
|
function defaultSessionLaunch(instance) {
|
|
1414
1704
|
const runtime = String(instance?.runtime_posture?.kind ?? instance?.runtime ?? '').toLowerCase();
|
|
1415
1705
|
if (runtime === 'host') {
|
|
@@ -2518,8 +2808,51 @@ export function createBridge({
|
|
|
2518
2808
|
if (url.pathname === '/api/mcp/discovery' && req.method === 'GET') return json(res, 200, await getMcpDiscovery(upstreamUrl));
|
|
2519
2809
|
if (url.pathname === '/api/mcp' && req.method === 'POST') return proxyMcpRequest(req, res, upstreamUrl, MCP_TOKEN_FILE);
|
|
2520
2810
|
if (url.pathname === '/api/running') return json(res, 200, await getRunning(upstreamUrl));
|
|
2521
|
-
if (url.pathname === '/api/missions') return json(res, 200, await getMissions(upstreamUrl));
|
|
2811
|
+
if (url.pathname === '/api/missions' && req.method === 'GET') return json(res, 200, await getMissions(upstreamUrl));
|
|
2812
|
+
if (url.pathname === '/api/missions' && req.method === 'POST') {
|
|
2813
|
+
const parsed = await readJsonBody(req);
|
|
2814
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
2815
|
+
return json(res, 201, await dispatchMission(parsed.body, upstreamUrl));
|
|
2816
|
+
}
|
|
2522
2817
|
if (url.pathname === '/api/events/snapshot') return json(res, 200, await getEventSnapshot(upstreamUrl));
|
|
2818
|
+
if (url.pathname === '/api/activity/coverage' && req.method === 'POST') {
|
|
2819
|
+
const parsed = await readJsonBody(req);
|
|
2820
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
2821
|
+
try {
|
|
2822
|
+
const result = await activityProxy(upstreamUrl, 'coverage', parsed.body);
|
|
2823
|
+
await appendAudit('activity.coverage.queried', { scope: activityRequest(parsed.body).scope, complete: result.body?.completeness?.complete === true });
|
|
2824
|
+
return json(res, result.status, result.body);
|
|
2825
|
+
} catch (error) {
|
|
2826
|
+
return json(res, Number(error?.upstreamStatus) || (String(error?.code).startsWith('activity_') ? 400 : 502), { error: error?.code ?? 'activity_upstream_error', message: String(error?.message ?? error) });
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
if (url.pathname === '/api/activity/timeline' && req.method === 'POST') {
|
|
2830
|
+
const parsed = await readJsonBody(req);
|
|
2831
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
2832
|
+
try {
|
|
2833
|
+
const result = await activityProxy(upstreamUrl, 'timeline', parsed.body);
|
|
2834
|
+
if (result.status < 200 || result.status >= 300) return json(res, result.status, result.body);
|
|
2835
|
+
await appendAudit('activity.timeline.queried', { scope: activityRequest(parsed.body).scope, event_count: result.body.events.length, complete: result.body.completeness.complete });
|
|
2836
|
+
return json(res, result.status, result.body);
|
|
2837
|
+
} catch (error) {
|
|
2838
|
+
return json(res, Number(error?.upstreamStatus) || (String(error?.code).startsWith('activity_') ? 400 : 502), { error: error?.code ?? 'activity_upstream_error', message: String(error?.message ?? error) });
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
if (url.pathname === '/api/activity/export' && req.method === 'POST') {
|
|
2842
|
+
const parsed = await readJsonBody(req);
|
|
2843
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
2844
|
+
try {
|
|
2845
|
+
const result = await activityProxy(upstreamUrl, 'export', parsed.body);
|
|
2846
|
+
if (result.status === 503) return json(res, 503, { error: 'activity_export_unavailable', message: 'The sandbox signing key is unavailable.' });
|
|
2847
|
+
if (result.status < 200 || result.status >= 300) return json(res, result.status, result.body);
|
|
2848
|
+
await appendAudit('activity.export.completed', { scope: activityRequest(parsed.body).scope, key_id: result.body.manifest.key_id, merkle_root: result.body.manifest.merkle_root, event_count: result.body.manifest.event_count });
|
|
2849
|
+
res.setHeader('content-disposition', 'attachment; filename="activity-export.json"');
|
|
2850
|
+
res.setHeader('cache-control', 'no-store');
|
|
2851
|
+
return json(res, result.status, result.body);
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
return json(res, Number(error?.upstreamStatus) || (String(error?.code).startsWith('activity_') ? 400 : 502), { error: error?.code ?? 'activity_upstream_error', message: String(error?.message ?? error) });
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2523
2856
|
if (url.pathname === '/api/loadouts') return json(res, 200, await getLoadouts(upstreamUrl));
|
|
2524
2857
|
if (url.pathname === '/api/index/status' && req.method === 'GET') return json(res, 200, await getIndexStatus());
|
|
2525
2858
|
if (url.pathname === '/api/index/query' && req.method === 'GET') {
|
|
@@ -2530,6 +2863,19 @@ export function createBridge({
|
|
|
2530
2863
|
const result = await rebuildIndex(req);
|
|
2531
2864
|
return json(res, result.status, result.body);
|
|
2532
2865
|
}
|
|
2866
|
+
if (url.pathname === '/api/index/graphs' && req.method === 'POST') {
|
|
2867
|
+
const parsed = await readJsonBody(req);
|
|
2868
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
2869
|
+
const requested = await appendAudit('index.graph.create.requested', { graph: parsed.body?.name ?? null });
|
|
2870
|
+
try {
|
|
2871
|
+
const graph = await createUserIndexGraph(parsed.body);
|
|
2872
|
+
await appendAudit('index.graph.create.completed', { request_ts: requested.ts, graph: graph.name });
|
|
2873
|
+
return json(res, 201, { ok: true, graph });
|
|
2874
|
+
} catch (error) {
|
|
2875
|
+
await appendAudit('index.graph.create.rejected', { request_ts: requested.ts, reason: String(error?.message ?? error) });
|
|
2876
|
+
return json(res, 400, { error: 'invalid_graph_definition', detail: String(error?.message ?? error) });
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2533
2879
|
if (url.pathname === '/api/audit' && req.method === 'GET') {
|
|
2534
2880
|
const limit = Math.max(1, Math.min(200, Number(url.searchParams.get('limit') || 50)));
|
|
2535
2881
|
return json(res, 200, { source: 'cockpit-bridge-audit/v1', audit: await readAudit({ limit }) });
|
|
@@ -2580,8 +2926,9 @@ export function createBridge({
|
|
|
2580
2926
|
body: requestBody,
|
|
2581
2927
|
},
|
|
2582
2928
|
]).catch((err) => ({ status: 502, body: { error: 'bridge_upstream_error', message: String(err?.message ?? err) } }));
|
|
2583
|
-
|
|
2584
|
-
|
|
2929
|
+
const projected = managedDockerLaunchError(result.status, result.body);
|
|
2930
|
+
await appendAudit('instance.launch.result', { request_ts: before.ts, status: projected.status, result: projected.body });
|
|
2931
|
+
return json(res, projected.status, projected.body);
|
|
2585
2932
|
}
|
|
2586
2933
|
if ((m = url.pathname.match(/^\/api\/operations\/([^/]+)$/)) && req.method === 'GET') {
|
|
2587
2934
|
return proxyFirst(res, [
|
|
@@ -2620,7 +2967,7 @@ export function createBridge({
|
|
|
2620
2967
|
if (type && type !== 'all') {
|
|
2621
2968
|
const types = type.split(',').map((t) => t.trim()).filter(Boolean);
|
|
2622
2969
|
if (!types.length || types.some((t) => !CAPABILITY_TYPES.has(t))) {
|
|
2623
|
-
return json(res, 400, { error: 'invalid_type', detail:
|
|
2970
|
+
return json(res, 400, { error: 'invalid_type', detail: `type must be all or a comma list of: ${[...CAPABILITY_TYPES].join(', ')}` });
|
|
2624
2971
|
}
|
|
2625
2972
|
args.push('--type', types.join(','));
|
|
2626
2973
|
}
|
|
@@ -2821,6 +3168,29 @@ export function createBridge({
|
|
|
2821
3168
|
await appendAudit('instance.destroy.requested', { instance_id: decodeURIComponent(m[1]), status, result: body });
|
|
2822
3169
|
return json(res, status, body);
|
|
2823
3170
|
}
|
|
3171
|
+
if ((m = url.pathname.match(/^\/api\/missions\/([^/]+)\/(pause|resume)$/)) && req.method === 'POST') {
|
|
3172
|
+
const parsed = await readJsonBody(req);
|
|
3173
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
3174
|
+
const result = await controlMission({
|
|
3175
|
+
action: m[2],
|
|
3176
|
+
sessionId: decodeURIComponent(m[1]),
|
|
3177
|
+
expectedUpdatedAt: parsed.body?.expected_updated_at,
|
|
3178
|
+
requestId: parsed.body?.request_id,
|
|
3179
|
+
});
|
|
3180
|
+
return json(res, 200, { ...result, projection: await getMissions(upstreamUrl) });
|
|
3181
|
+
}
|
|
3182
|
+
if ((m = url.pathname.match(/^\/api\/missions\/([^/]+)\/([^/]+)\/cancel$/)) && req.method === 'POST') {
|
|
3183
|
+
const parsed = await readJsonBody(req);
|
|
3184
|
+
if (parsed.error) return json(res, 400, { error: parsed.error });
|
|
3185
|
+
const result = await controlMission({
|
|
3186
|
+
action: 'cancel',
|
|
3187
|
+
sessionId: decodeURIComponent(m[1]),
|
|
3188
|
+
missionId: decodeURIComponent(m[2]),
|
|
3189
|
+
expectedUpdatedAt: parsed.body?.expected_updated_at,
|
|
3190
|
+
requestId: parsed.body?.request_id,
|
|
3191
|
+
});
|
|
3192
|
+
return json(res, 200, { ...result, projection: await getMissions(upstreamUrl) });
|
|
3193
|
+
}
|
|
2824
3194
|
if ((m = url.pathname.match(/^\/api\/tasks\/([^/]+)\/([^/]+)\/cancel$/)) && req.method === 'POST') {
|
|
2825
3195
|
await appendAudit('task.cancel.requested', { instance_id: decodeURIComponent(m[1]), task_id: decodeURIComponent(m[2]) });
|
|
2826
3196
|
return proxy(res, 'POST', `${upstreamUrl}/agents/${encodeURIComponent(m[1])}/tasks/${encodeURIComponent(m[2])}:cancel`);
|
|
@@ -2867,7 +3237,7 @@ export function createBridge({
|
|
|
2867
3237
|
}
|
|
2868
3238
|
json(res, 404, { error: 'not_found', path: url.pathname });
|
|
2869
3239
|
} catch (err) {
|
|
2870
|
-
const status = Number(err?.upstreamStatus) || 502;
|
|
3240
|
+
const status = Number(err?.status) || Number(err?.upstreamStatus) || 502;
|
|
2871
3241
|
json(res, status, { error: err?.code ?? 'bridge_upstream_error', message: String(err?.message ?? err) });
|
|
2872
3242
|
}
|
|
2873
3243
|
};
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"required": ["command"],
|
|
32
32
|
"additionalProperties": false,
|
|
33
33
|
"properties": {
|
|
34
|
-
"command": { "type": "string", "description": "command
|
|
34
|
+
"command": { "type": "string", "pattern": "^/[a-zA-Z0-9][a-zA-Z0-9._-]*(\\s[^\\r\\n]*)?$", "description": "single slash command injected into an agentic session; newlines are forbidden" },
|
|
35
35
|
"target": { "type": "string", "enum": ["focused", "new"], "default": "focused", "description": "focused = inject into the attached session, else offer a new one; new = always a fresh session" },
|
|
36
36
|
"needs_args": { "type": "boolean", "description": "prompt for arguments before injecting" },
|
|
37
37
|
"args_hint": { "type": "string" }
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"properties": {
|
|
50
50
|
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._-]{0,63}$" },
|
|
51
51
|
"title": { "type": "string" },
|
|
52
|
-
"source": { "type": "string", "description": "
|
|
52
|
+
"source": { "type": "string", "pattern": "^(cockpit|sandbox)://", "description": "first-party cockpit:// route or third-party sandbox://<contribution-id>/ resource" }
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cockpit",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.5",
|
|
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",
|