@aiwg/cockpit 2026.8.4 → 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 +190 -2
- package/package.json +1 -1
- 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 +1 -1
- package/web/src/App.tsx +3 -0
- package/web/src/api.ts +7 -1
- package/web/src/components/Activity.test.tsx +38 -0
- package/web/src/components/Activity.tsx +78 -0
- 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/styles.css +8 -0
- package/web/src/types.ts +14 -0
- package/web/dist/assets/index-DNlFgn6L.js +0 -312
package/bridge/src/server.mjs
CHANGED
|
@@ -1533,6 +1533,7 @@ function normalizeInstance(executorUrl, i) {
|
|
|
1533
1533
|
? { mode: i.transport, trust: i.transport_posture, source: 'agentic-sandbox admin-v2' }
|
|
1534
1534
|
: i.transport ?? i.transport_posture ?? i.security_posture ?? i.security?.transport,
|
|
1535
1535
|
),
|
|
1536
|
+
managed_docker_posture: normalizeManagedDockerPosture(i, runtimePosture.kind),
|
|
1536
1537
|
launch_context: {
|
|
1537
1538
|
cwd: i.launch_context?.cwd ?? i.launchContext?.cwd ?? i.cwd,
|
|
1538
1539
|
loadout,
|
|
@@ -1551,6 +1552,154 @@ function normalizeInstance(executorUrl, i) {
|
|
|
1551
1552
|
};
|
|
1552
1553
|
}
|
|
1553
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
|
+
|
|
1554
1703
|
function defaultSessionLaunch(instance) {
|
|
1555
1704
|
const runtime = String(instance?.runtime_posture?.kind ?? instance?.runtime ?? '').toLowerCase();
|
|
1556
1705
|
if (runtime === 'host') {
|
|
@@ -2666,6 +2815,44 @@ export function createBridge({
|
|
|
2666
2815
|
return json(res, 201, await dispatchMission(parsed.body, upstreamUrl));
|
|
2667
2816
|
}
|
|
2668
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
|
+
}
|
|
2669
2856
|
if (url.pathname === '/api/loadouts') return json(res, 200, await getLoadouts(upstreamUrl));
|
|
2670
2857
|
if (url.pathname === '/api/index/status' && req.method === 'GET') return json(res, 200, await getIndexStatus());
|
|
2671
2858
|
if (url.pathname === '/api/index/query' && req.method === 'GET') {
|
|
@@ -2739,8 +2926,9 @@ export function createBridge({
|
|
|
2739
2926
|
body: requestBody,
|
|
2740
2927
|
},
|
|
2741
2928
|
]).catch((err) => ({ status: 502, body: { error: 'bridge_upstream_error', message: String(err?.message ?? err) } }));
|
|
2742
|
-
|
|
2743
|
-
|
|
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);
|
|
2744
2932
|
}
|
|
2745
2933
|
if ((m = url.pathname.match(/^\/api\/operations\/([^/]+)$/)) && req.method === 'GET') {
|
|
2746
2934
|
return proxyFirst(res, [
|
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",
|