@aiwg/cockpit 2026.8.4 → 2026.8.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.
package/README.md CHANGED
@@ -411,7 +411,11 @@ safe version/build fields in the report. When the executor build does not expose
411
411
  that metadata, set `AIWG_COCKPIT_EXECUTOR_VERSION=<tag-or-commit>` so release
412
412
  evidence still names the tested agentic-sandbox build. A manual run against
413
413
  agentic-sandbox `v2026.6.15` or newer should attach its markdown/JSON result to
414
- epic roctinam/aiwg#1588 before the epic is considered done-done.
414
+ epic roctinam/aiwg#1588 before the epic is considered done-done. The latest
415
+ immutable fleet/activity qualification is
416
+ [Agentic Sandbox v2026.8.3](../../docs/cockpit/qualifications/agentic-sandbox-v2026.8.3.md);
417
+ older v2026.7.x entries below are historical feature-floor evidence, not the
418
+ latest whole-integration claim.
415
419
 
416
420
  The executable release-validation procedure is
417
421
  `.aiwg/testing/cockpit-real-integration-uat-runbook.md`. Use that runbook for
@@ -575,7 +579,7 @@ agentic-sandbox executor through `AIWG_COCKPIT_EXECUTOR_URL`. The host target
575
579
  (agentic-sandbox#461) have landed upstream; the Bridge seam is now the
576
580
  AIWG-side integration point for #1589. Runtime-tier provisioning and
577
581
  host-daemon surfacing (roctinam/aiwg#1615) and transport-trust visibility (#1618)
578
- **landed and are verified against `v2026.7.4`** — transport posture and
582
+ **landed with a compatibility floor verified against `v2026.7.4`** — transport posture and
579
583
  host-daemon now render per instance (a host-daemon *detail-status* payload
580
584
  remains a residual under #1615). Direct/managed PTY negotiation (#1616) and the
581
585
  live real-sandbox gate (#1617) continue. Secure transport details map back to agentic-sandbox#409/#410/#412; local
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/jmagly/agentic-sandbox/blob/main/docs/schemas/activity-event-v1.schema.json",
4
+ "title": "Agentic Sandbox Activity Event v1",
5
+ "description": "Stable metadata-first, loss-aware activity event envelope.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "event_id", "event_name", "plane", "occurred_at", "observed_at", "source", "correlation", "sensitivity", "retention_class", "payload", "integrity"],
9
+ "properties": {
10
+ "schema_version": {"const": "activity.event/v1"},
11
+ "event_id": {"type": "string", "format": "uuid", "description": "Source-generated UUIDv7."},
12
+ "event_name": {"type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$"},
13
+ "plane": {"enum": ["session", "action", "network", "runtime", "system", "integrity"]},
14
+ "occurred_at": {"type": "string", "format": "date-time"},
15
+ "observed_at": {"type": "string", "format": "date-time"},
16
+ "source": {"$ref": "#/$defs/source"},
17
+ "correlation": {"$ref": "#/$defs/correlation"},
18
+ "actor": {"$ref": "#/$defs/entity"},
19
+ "target": {"$ref": "#/$defs/entity"},
20
+ "outcome": {"$ref": "#/$defs/outcome"},
21
+ "sensitivity": {"enum": ["metadata", "restricted-content", "secret-prohibited"]},
22
+ "retention_class": {"enum": ["standard", "security", "forensic-hold", "ephemeral"]},
23
+ "payload": {"type": "object", "description": "Event-specific allowlisted metadata after source filtering."},
24
+ "integrity": {"$ref": "#/$defs/integrity"}
25
+ },
26
+ "$defs": {
27
+ "source": {
28
+ "type": "object", "additionalProperties": false,
29
+ "required": ["collector", "layer", "runtime", "trust"],
30
+ "properties": {
31
+ "collector": {"type": "string", "minLength": 1},
32
+ "layer": {"enum": ["guest", "runtime", "host", "control-plane", "provider"]},
33
+ "runtime": {"enum": ["qemu-kvm", "cloud-hypervisor", "docker", "host", "unknown"]},
34
+ "trust": {"enum": ["observed", "attested", "self-reported", "derived"]},
35
+ "clock_id": {"type": "string"},
36
+ "clock_error_ms": {"type": "number", "minimum": 0}
37
+ }
38
+ },
39
+ "correlation": {
40
+ "type": "object", "additionalProperties": false,
41
+ "required": ["tenant_id", "host_id", "instance_id", "agent_id"],
42
+ "properties": {
43
+ "tenant_id": {"type": "string", "minLength": 1}, "host_id": {"type": "string", "minLength": 1},
44
+ "instance_id": {"type": "string", "minLength": 1}, "agent_id": {"type": "string", "minLength": 1},
45
+ "session_id": {"type": "string"}, "mission_id": {"type": "string"}, "task_id": {"type": "string"},
46
+ "tool_call_id": {"type": "string"}, "command_id": {"type": "string"}, "process_id": {"type": "string"},
47
+ "parent_event_id": {"type": "string", "format": "uuid"},
48
+ "trace_id": {"type": "string", "pattern": "^[0-9a-f]{32}$"},
49
+ "span_id": {"type": "string", "pattern": "^[0-9a-f]{16}$"}
50
+ }
51
+ },
52
+ "entity": {
53
+ "type": "object", "additionalProperties": false, "required": ["type", "id"],
54
+ "properties": {"type": {"type": "string"}, "id": {"type": "string"}, "uid": {"type": "integer", "minimum": 0}, "gid": {"type": "integer", "minimum": 0}, "pid": {"type": "integer", "minimum": 0}}
55
+ },
56
+ "outcome": {
57
+ "type": "object", "additionalProperties": false, "required": ["status"],
58
+ "properties": {"status": {"enum": ["started", "success", "failure", "allowed", "denied", "degraded", "unknown"]}, "exit_code": {"type": "integer"}, "reason": {"type": "string"}}
59
+ },
60
+ "integrity": {
61
+ "type": "object", "additionalProperties": false, "required": ["collector_sequence"],
62
+ "properties": {
63
+ "collector_sequence": {"type": "integer", "minimum": 1},
64
+ "source_hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "source_previous_hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
65
+ "timeline_hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "timeline_previous_hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
66
+ "signature": {"type": "string"}, "key_id": {"type": "string"}
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://agentic-sandbox.local/schemas/activity-operational-evidence-v1.schema.json",
4
+ "title": "Agentic Sandbox daily operational evidence record v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "schema_version",
9
+ "record_date",
10
+ "sealed_at",
11
+ "window",
12
+ "identity_digests",
13
+ "input_digest",
14
+ "previous_record_digest",
15
+ "summary",
16
+ "record_digest"
17
+ ],
18
+ "properties": {
19
+ "schema_version": {
20
+ "const": "agentic.activity-operational-evidence/v1"
21
+ },
22
+ "record_date": {
23
+ "type": "string",
24
+ "format": "date"
25
+ },
26
+ "sealed_at": {
27
+ "type": "string",
28
+ "format": "date-time"
29
+ },
30
+ "window": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["start", "end", "actual_span_seconds"],
34
+ "properties": {
35
+ "start": {"type": "string", "format": "date-time"},
36
+ "end": {"type": "string", "format": "date-time"},
37
+ "actual_span_seconds": {"type": "number", "minimum": 0}
38
+ }
39
+ },
40
+ "identity_digests": {
41
+ "type": "array",
42
+ "items": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
43
+ "minItems": 1,
44
+ "uniqueItems": true
45
+ },
46
+ "input_digest": {
47
+ "type": "string",
48
+ "pattern": "^[0-9a-f]{64}$"
49
+ },
50
+ "previous_record_digest": {
51
+ "oneOf": [
52
+ {"type": "null"},
53
+ {"type": "string", "pattern": "^[0-9a-f]{64}$"}
54
+ ]
55
+ },
56
+ "summary": {
57
+ "type": "object",
58
+ "required": [
59
+ "sample_count",
60
+ "evidence_class_counts",
61
+ "source_availability",
62
+ "latency_ms",
63
+ "metrics"
64
+ ],
65
+ "properties": {
66
+ "sample_count": {"type": "integer", "minimum": 1},
67
+ "evidence_class_counts": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "required": ["organic", "canary", "drill"],
71
+ "properties": {
72
+ "organic": {"type": "integer", "minimum": 0},
73
+ "canary": {"type": "integer", "minimum": 0},
74
+ "drill": {"type": "integer", "minimum": 0}
75
+ }
76
+ },
77
+ "source_availability": {"type": "object"},
78
+ "latency_ms": {"type": "object"},
79
+ "metrics": {"type": "object"}
80
+ },
81
+ "additionalProperties": false
82
+ },
83
+ "record_digest": {
84
+ "type": "string",
85
+ "pattern": "^[0-9a-f]{64}$"
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ const EVENT_SCHEMA = JSON.parse(readFileSync(
4
+ new URL('../contracts/activity-event-v1.schema.json', import.meta.url),
5
+ 'utf8',
6
+ ));
7
+
8
+ const RESTRICTED_KEY = /(?:^|_)(?:content|terminal|prompt|environment|env|credential|secret|password|authorization|bearer|token|private_key|certificate|restricted_(?:url|uri|link))(?:$|_)/i;
9
+
10
+ function resolveRef(root, ref) {
11
+ if (!ref.startsWith('#/')) throw new Error(`unsupported external schema reference: ${ref}`);
12
+ return ref.slice(2).split('/').reduce((value, segment) => value?.[segment.replaceAll('~1', '/').replaceAll('~0', '~')], root);
13
+ }
14
+
15
+ function typeMatches(value, type) {
16
+ if (type === 'null') return value === null;
17
+ if (type === 'array') return Array.isArray(value);
18
+ if (type === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
19
+ if (type === 'integer') return Number.isInteger(value);
20
+ if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
21
+ return typeof value === type;
22
+ }
23
+
24
+ function formatMatches(value, format) {
25
+ if (format === 'uuid') return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
26
+ if (format === 'date') return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(`${value}T00:00:00Z`));
27
+ if (format === 'date-time') return !Number.isNaN(Date.parse(value));
28
+ return true;
29
+ }
30
+
31
+ function validateNode(value, schema, root, path, errors) {
32
+ if (schema.$ref) return validateNode(value, resolveRef(root, schema.$ref), root, path, errors);
33
+ if (schema.oneOf) {
34
+ const matches = schema.oneOf.filter((candidate) => {
35
+ const candidateErrors = [];
36
+ validateNode(value, candidate, root, path, candidateErrors);
37
+ return candidateErrors.length === 0;
38
+ });
39
+ if (matches.length !== 1) errors.push(`${path} must match exactly one schema`);
40
+ return;
41
+ }
42
+ if (schema.const !== undefined && value !== schema.const) errors.push(`${path} must equal ${JSON.stringify(schema.const)}`);
43
+ if (schema.enum && !schema.enum.includes(value)) errors.push(`${path} is not an allowed value`);
44
+ if (schema.type && !typeMatches(value, schema.type)) {
45
+ errors.push(`${path} must be ${schema.type}`);
46
+ return;
47
+ }
48
+ if (typeof value === 'string') {
49
+ if (schema.minLength !== undefined && value.length < schema.minLength) errors.push(`${path} is too short`);
50
+ if (schema.pattern && !new RegExp(schema.pattern, 'u').test(value)) errors.push(`${path} has invalid format`);
51
+ if (schema.format && !formatMatches(value, schema.format)) errors.push(`${path} has invalid ${schema.format} format`);
52
+ }
53
+ if (typeof value === 'number' && schema.minimum !== undefined && value < schema.minimum) errors.push(`${path} is below minimum`);
54
+ if (Array.isArray(value)) {
55
+ if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${path} has too few items`);
56
+ if (schema.uniqueItems && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) errors.push(`${path} items must be unique`);
57
+ if (schema.items) value.forEach((item, index) => validateNode(item, schema.items, root, `${path}/${index}`, errors));
58
+ }
59
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
60
+ for (const key of schema.required ?? []) if (!(key in value)) errors.push(`${path}/${key} is required`);
61
+ if (schema.additionalProperties === false) {
62
+ for (const key of Object.keys(value)) if (!(key in (schema.properties ?? {}))) errors.push(`${path}/${key} is not allowed`);
63
+ }
64
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
65
+ if (key in value) validateNode(value[key], childSchema, root, `${path}/${key}`, errors);
66
+ }
67
+ }
68
+ }
69
+
70
+ function containsRestrictedField(value) {
71
+ if (Array.isArray(value)) return value.some(containsRestrictedField);
72
+ if (!value || typeof value !== 'object') return false;
73
+ return Object.entries(value).some(([key, child]) => RESTRICTED_KEY.test(key) || containsRestrictedField(child));
74
+ }
75
+
76
+ export function validateActivityEvent(event, expectedScope = undefined) {
77
+ const errors = [];
78
+ validateNode(event, EVENT_SCHEMA, EVENT_SCHEMA, '$', errors);
79
+ if (event?.sensitivity !== 'metadata') errors.push('$/sensitivity must be metadata at the Cockpit boundary');
80
+ if (containsRestrictedField(event)) errors.push('$ contains a restricted field');
81
+ if (expectedScope) {
82
+ for (const [key, value] of Object.entries(expectedScope)) {
83
+ if (event?.correlation?.[key] !== value) errors.push(`$/correlation/${key} scope mismatch`);
84
+ }
85
+ }
86
+ return { valid: errors.length === 0, errors };
87
+ }
88
+
89
+ export function assertActivityEvent(event, expectedScope = undefined) {
90
+ const result = validateActivityEvent(event, expectedScope);
91
+ if (!result.valid) {
92
+ const scopeMismatch = result.errors.some((error) => error.includes('scope mismatch'));
93
+ const restricted = result.errors.some((error) => error.includes('restricted') || error.includes('sensitivity'));
94
+ const error = new Error(result.errors.join('; '));
95
+ error.code = scopeMismatch ? 'activity_scope_mismatch' : restricted ? 'activity_restricted_data' : 'activity_malformed_envelope';
96
+ throw error;
97
+ }
98
+ return event;
99
+ }
100
+
101
+ export { EVENT_SCHEMA };
@@ -16,6 +16,7 @@ import { homedir } from 'node:os';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { dirname, join, basename, extname, resolve, sep } from 'node:path';
18
18
  import { storeCockpitToken } from '../../shell-core/keychain.mjs';
19
+ import { assertActivityEvent } from './activity-contract.mjs';
19
20
 
20
21
  const __dir = dirname(fileURLToPath(import.meta.url));
21
22
  // Primary seam for roctinam/aiwg#1589: Cockpit talks to a real agentic-sandbox
@@ -1533,6 +1534,7 @@ function normalizeInstance(executorUrl, i) {
1533
1534
  ? { mode: i.transport, trust: i.transport_posture, source: 'agentic-sandbox admin-v2' }
1534
1535
  : i.transport ?? i.transport_posture ?? i.security_posture ?? i.security?.transport,
1535
1536
  ),
1537
+ managed_docker_posture: normalizeManagedDockerPosture(i, runtimePosture.kind),
1536
1538
  launch_context: {
1537
1539
  cwd: i.launch_context?.cwd ?? i.launchContext?.cwd ?? i.cwd,
1538
1540
  loadout,
@@ -1551,6 +1553,151 @@ function normalizeInstance(executorUrl, i) {
1551
1553
  };
1552
1554
  }
1553
1555
 
1556
+ const MANAGED_DOCKER_CONTROL_UID_MIN = 200_000;
1557
+ const MANAGED_DOCKER_CONTROL_UID_MAX = 799_999;
1558
+ const MANAGED_DOCKER_WORKLOAD_UID = 10_001;
1559
+
1560
+ /** Project only executor-attested, client-safe managed-Docker identity evidence. */
1561
+ export function normalizeManagedDockerPosture(i, runtimeKind) {
1562
+ if (!['docker', 'container'].includes(String(runtimeKind).toLowerCase())) return undefined;
1563
+ const source = i.managed_docker_posture ?? i.managedDockerPosture ?? i.security_posture ?? i.securityPosture ?? i;
1564
+ const rawTransport = source.transport_mode ?? source.transportMode
1565
+ ?? (typeof source.transport === 'string' ? source.transport : source.transport?.mode)
1566
+ ?? (typeof i.transport === 'string' ? i.transport : i.transport?.mode)
1567
+ ?? 'unknown';
1568
+ const transportMode = String(rawTransport).toLowerCase();
1569
+ const rawControlUid = source.control_uid ?? source.controlUid;
1570
+ const controlUid = Number.isInteger(Number(rawControlUid)) ? Number(rawControlUid) : undefined;
1571
+ const rawWorkloadUid = source.workload_uid ?? source.workloadUid;
1572
+ const workloadUid = Number.isInteger(Number(rawWorkloadUid)) ? Number(rawWorkloadUid) : undefined;
1573
+ const boundary = String(source.workload_boundary ?? source.workloadBoundary ?? source.boundary ?? 'unknown').toLowerCase();
1574
+ const reportedFallback = String(source.fallback_reason_code ?? source.fallbackReasonCode ?? source.fallback_reason ?? source.fallbackReason ?? '').toLowerCase();
1575
+ const fallbackReason = transportMode === 'mtls-bootstrap' || reportedFallback === 'docker_desktop_peer_uid_unavailable'
1576
+ ? 'Docker Desktop UDS bridge does not preserve peer UID'
1577
+ : reportedFallback === 'identity_resolver_unavailable'
1578
+ ? 'Managed UDS identity resolver unavailable'
1579
+ : ['operator-configured', 'explicit', 'mtls'].includes(transportMode)
1580
+ ? 'Operator-configured compatibility transport'
1581
+ : undefined;
1582
+ const controlIdentityPresent = controlUid !== undefined;
1583
+ const controlIdentityRangeValid = controlIdentityPresent
1584
+ && controlUid >= MANAGED_DOCKER_CONTROL_UID_MIN
1585
+ && controlUid <= MANAGED_DOCKER_CONTROL_UID_MAX;
1586
+ const workloadIdentitySeparated = boundary === 'separated' && workloadUid === MANAGED_DOCKER_WORKLOAD_UID;
1587
+ const secureDefault = transportMode === 'uds' && controlIdentityRangeValid && workloadIdentitySeparated;
1588
+ const compatibility = transportMode !== 'uds';
1589
+ const requiresRecreation = !controlIdentityPresent || !workloadUid || boundary === 'unknown';
1590
+ return {
1591
+ transport_mode: transportMode,
1592
+ control_identity_present: controlIdentityPresent,
1593
+ control_identity_range_valid: controlIdentityRangeValid,
1594
+ workload_uid: workloadUid,
1595
+ workload_identity_separated: workloadIdentitySeparated,
1596
+ boundary,
1597
+ secure_default: secureDefault,
1598
+ compatibility,
1599
+ fallback_reason: fallbackReason ? String(fallbackReason).slice(0, 300) : undefined,
1600
+ requires_recreation: requiresRecreation,
1601
+ source: 'agentic-sandbox',
1602
+ };
1603
+ }
1604
+
1605
+ const ACTIVITY_SCOPE_HEADERS = {
1606
+ tenant_id: 'x-agentic-tenant-id', host_id: 'x-agentic-host-id',
1607
+ instance_id: 'x-agentic-instance-id', agent_id: 'x-agentic-agent-id',
1608
+ };
1609
+ 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']);
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
+ export function validateActivityEnvelope(body, expectedScope, { includeEvents = false, exportEnvelope = false } = {}) {
1634
+ if (!body || typeof body !== 'object' || Array.isArray(body)) throw Object.assign(new Error('malformed activity envelope'), { code: 'activity_malformed_envelope' });
1635
+ if (!exportEnvelope && body.schema_version !== 'activity.event/v1') throw Object.assign(new Error('unsupported activity schema'), { code: 'activity_malformed_envelope' });
1636
+ const events = Array.isArray(body.events) ? body.events : [];
1637
+ if (includeEvents && !Array.isArray(body.events)) throw Object.assign(new Error('activity envelope has no events array'), { code: 'activity_malformed_envelope' });
1638
+ if (!exportEnvelope && (!Array.isArray(body.coverage) || !body.completeness || typeof body.completeness.complete !== 'boolean')) {
1639
+ throw Object.assign(new Error('activity envelope has invalid coverage'), { code: 'activity_malformed_envelope' });
1640
+ }
1641
+ const nonnegativeInteger = (value) => Number.isInteger(value) && value >= 0;
1642
+ const nonnegativeFinite = (value) => Number.isFinite(value) && value >= 0;
1643
+ const validCompleteness = (value) => value
1644
+ && typeof value.label === 'string'
1645
+ && nonnegativeInteger(value.collector_count)
1646
+ && nonnegativeInteger(value.sequence_gap_count)
1647
+ && nonnegativeInteger(value.durable_loss_count)
1648
+ && nonnegativeInteger(value.restart_count)
1649
+ && nonnegativeInteger(value.dropped_event_count)
1650
+ && nonnegativeInteger(value.stale_collector_count)
1651
+ && Array.isArray(value.unsupported_event_classes)
1652
+ && value.unsupported_event_classes.every((item) => typeof item === 'string')
1653
+ && nonnegativeFinite(value.maximum_clock_error_ms);
1654
+ if (!exportEnvelope && !validCompleteness(body.completeness)) {
1655
+ throw Object.assign(new Error('activity envelope has malformed completeness summary'), { code: 'activity_malformed_envelope' });
1656
+ }
1657
+ 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))) {
1658
+ throw Object.assign(new Error('activity envelope has malformed collector coverage'), { code: 'activity_malformed_envelope' });
1659
+ }
1660
+ for (const event of events) {
1661
+ assertActivityEvent(event, expectedScope);
1662
+ }
1663
+ const manifest = body.manifest;
1664
+ if (exportEnvelope && (!manifest
1665
+ || typeof manifest.batch_id !== 'string' || !manifest.batch_id
1666
+ || manifest.tenant_id !== expectedScope.tenant_id
1667
+ || typeof manifest.collector_id !== 'string' || !manifest.collector_id
1668
+ || !Number.isInteger(manifest.event_count) || manifest.event_count < 0
1669
+ || !/^[0-9a-f]{64}$/.test(manifest.merkle_root ?? '')
1670
+ || typeof manifest.key_id !== 'string' || !manifest.key_id
1671
+ || typeof manifest.signature !== 'string' || !manifest.signature
1672
+ || (manifest.previous_root !== null && manifest.previous_root !== undefined && !/^[0-9a-f]{64}$/.test(manifest.previous_root)))) {
1673
+ throw Object.assign(new Error('signed activity export has no valid manifest'), { code: 'activity_malformed_export' });
1674
+ }
1675
+ return body;
1676
+ }
1677
+
1678
+ async function activityProxy(executorUrl, kind, input) {
1679
+ const request = activityRequest(input);
1680
+ const isExport = kind === 'export';
1681
+ const query = new URLSearchParams(Object.entries(request.filter).map(([key, value]) => [key, String(value)]));
1682
+ const target = `${executorUrl}/api/v2/activity/${kind}${!isExport && query.size ? `?${query}` : ''}`;
1683
+ const result = await fetchJsonFirst([{ target, method: isExport ? 'POST' : 'GET', headers: { ...request.headers, ...(isExport ? { 'content-type': 'application/json' } : {}) }, body: isExport ? JSON.stringify(request.filter) : undefined }]);
1684
+ if (!result.status.toString().startsWith('2')) return result;
1685
+ return { ...result, body: validateActivityEnvelope(result.body, request.scope, { includeEvents: kind === 'timeline' || isExport, exportEnvelope: isExport }) };
1686
+ }
1687
+
1688
+ function managedDockerLaunchError(status, body) {
1689
+ const detail = String(body?.message ?? body?.error?.message ?? body?.error ?? body?.failure?.message ?? '');
1690
+ if (/refuses startup profiles that materialize raw credential refs/i.test(detail)) return {
1691
+ status: status >= 400 ? status : 422,
1692
+ body: {
1693
+ error: 'managed_docker_raw_credentials_rejected',
1694
+ message: 'Managed Docker does not accept startup profiles with raw credential references.',
1695
+ recovery: 'Use the sandbox credential proxy or select a VM runtime. Cockpit will not downgrade the transport automatically.',
1696
+ },
1697
+ };
1698
+ return { status, body };
1699
+ }
1700
+
1554
1701
  function defaultSessionLaunch(instance) {
1555
1702
  const runtime = String(instance?.runtime_posture?.kind ?? instance?.runtime ?? '').toLowerCase();
1556
1703
  if (runtime === 'host') {
@@ -2666,6 +2813,44 @@ export function createBridge({
2666
2813
  return json(res, 201, await dispatchMission(parsed.body, upstreamUrl));
2667
2814
  }
2668
2815
  if (url.pathname === '/api/events/snapshot') return json(res, 200, await getEventSnapshot(upstreamUrl));
2816
+ if (url.pathname === '/api/activity/coverage' && req.method === 'POST') {
2817
+ const parsed = await readJsonBody(req);
2818
+ if (parsed.error) return json(res, 400, { error: parsed.error });
2819
+ try {
2820
+ const result = await activityProxy(upstreamUrl, 'coverage', parsed.body);
2821
+ await appendAudit('activity.coverage.queried', { scope: activityRequest(parsed.body).scope, complete: result.body?.completeness?.complete === true });
2822
+ return json(res, result.status, result.body);
2823
+ } catch (error) {
2824
+ return json(res, Number(error?.upstreamStatus) || (String(error?.code).startsWith('activity_') ? 400 : 502), { error: error?.code ?? 'activity_upstream_error', message: String(error?.message ?? error) });
2825
+ }
2826
+ }
2827
+ if (url.pathname === '/api/activity/timeline' && req.method === 'POST') {
2828
+ const parsed = await readJsonBody(req);
2829
+ if (parsed.error) return json(res, 400, { error: parsed.error });
2830
+ try {
2831
+ const result = await activityProxy(upstreamUrl, 'timeline', parsed.body);
2832
+ if (result.status < 200 || result.status >= 300) return json(res, result.status, result.body);
2833
+ await appendAudit('activity.timeline.queried', { scope: activityRequest(parsed.body).scope, event_count: result.body.events.length, complete: result.body.completeness.complete });
2834
+ return json(res, result.status, result.body);
2835
+ } catch (error) {
2836
+ return json(res, Number(error?.upstreamStatus) || (String(error?.code).startsWith('activity_') ? 400 : 502), { error: error?.code ?? 'activity_upstream_error', message: String(error?.message ?? error) });
2837
+ }
2838
+ }
2839
+ if (url.pathname === '/api/activity/export' && req.method === 'POST') {
2840
+ const parsed = await readJsonBody(req);
2841
+ if (parsed.error) return json(res, 400, { error: parsed.error });
2842
+ try {
2843
+ const result = await activityProxy(upstreamUrl, 'export', parsed.body);
2844
+ if (result.status === 503) return json(res, 503, { error: 'activity_export_unavailable', message: 'The sandbox signing key is unavailable.' });
2845
+ if (result.status < 200 || result.status >= 300) return json(res, result.status, result.body);
2846
+ 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 });
2847
+ res.setHeader('content-disposition', 'attachment; filename="activity-export.json"');
2848
+ res.setHeader('cache-control', 'no-store');
2849
+ return json(res, result.status, result.body);
2850
+ } catch (error) {
2851
+ return json(res, Number(error?.upstreamStatus) || (String(error?.code).startsWith('activity_') ? 400 : 502), { error: error?.code ?? 'activity_upstream_error', message: String(error?.message ?? error) });
2852
+ }
2853
+ }
2669
2854
  if (url.pathname === '/api/loadouts') return json(res, 200, await getLoadouts(upstreamUrl));
2670
2855
  if (url.pathname === '/api/index/status' && req.method === 'GET') return json(res, 200, await getIndexStatus());
2671
2856
  if (url.pathname === '/api/index/query' && req.method === 'GET') {
@@ -2739,8 +2924,9 @@ export function createBridge({
2739
2924
  body: requestBody,
2740
2925
  },
2741
2926
  ]).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);
2927
+ const projected = managedDockerLaunchError(result.status, result.body);
2928
+ await appendAudit('instance.launch.result', { request_ts: before.ts, status: projected.status, result: projected.body });
2929
+ return json(res, projected.status, projected.body);
2744
2930
  }
2745
2931
  if ((m = url.pathname.match(/^\/api\/operations\/([^/]+)$/)) && req.method === 'GET') {
2746
2932
  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.7",
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",