@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.
@@ -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
- await appendAudit('instance.launch.result', { request_ts: before.ts, status: result.status, result: result.body });
2743
- return json(res, result.status, result.body);
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.4",
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",