@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 +6 -2
- package/bridge/contracts/activity-event-v1.schema.json +70 -0
- package/bridge/contracts/activity-operational-evidence-v1.schema.json +88 -0
- package/bridge/src/activity-contract.mjs +101 -0
- package/bridge/src/server.mjs +188 -2
- package/package.json +1 -1
- package/web/dist/assets/index-BugGiqP9.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/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
|
|
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 };
|
package/bridge/src/server.mjs
CHANGED
|
@@ -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
|
-
|
|
2743
|
-
|
|
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.
|
|
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",
|