@aiwg/cockpit 2026.8.16 → 2026.8.18
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 +5 -0
- package/bridge/src/server.mjs +299 -54
- package/bridge/src/smoke.mjs +17 -1
- package/package.json +1 -1
- package/web/dist/assets/index-RVY4M2_N.js +312 -0
- package/web/dist/index.html +1 -1
- package/web/src/App.test.tsx +45 -0
- package/web/src/components/Missions.tsx +45 -4
- package/web/src/types.ts +28 -0
- package/web/dist/assets/index-BLjy5K5V.js +0 -312
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# AIWG Cockpit
|
|
4
4
|
|
|
5
|
+
Cockpit's agentic-sandbox management API version, upstream A2A protocol
|
|
6
|
+
version, and extension `/v1` URI suffixes are independent. See
|
|
7
|
+
[A2A protocol compatibility](../../docs/a2a-protocol-compatibility.md) for the
|
|
8
|
+
0.3/1.0 selection and qualification contract.
|
|
9
|
+
|
|
5
10
|
**Local control plane for AIWG and multi-stack agentic sessions**
|
|
6
11
|
|
|
7
12
|
Observe live agent work, attach to sessions, handle approvals, launch runtime
|
package/bridge/src/server.mjs
CHANGED
|
@@ -33,6 +33,8 @@ const EXECUTOR_TOKEN_FILE = process.env.AIWG_COCKPIT_EXECUTOR_TOKEN_FILE ?? '';
|
|
|
33
33
|
const MCP_TOKEN_FILE = process.env.AIWG_COCKPIT_MCP_TOKEN_FILE ?? '';
|
|
34
34
|
const LOCAL_DOCKER_FALLBACK = process.env.AIWG_COCKPIT_LOCAL_DOCKER_FALLBACK === '1';
|
|
35
35
|
const REQUIRE_SANDBOX_MTLS = process.env.AIWG_COCKPIT_REQUIRE_SANDBOX_MTLS === '1';
|
|
36
|
+
const COCKPIT_A2A_PROTOCOL_POLICY = process.env.AIWG_COCKPIT_A2A_PROTOCOL_POLICY ?? '0.3';
|
|
37
|
+
const COCKPIT_A2A_PROTOCOL_FALLBACK = process.env.AIWG_COCKPIT_A2A_PROTOCOL_FALLBACK === '1';
|
|
36
38
|
export function localLibvirtFallbackAllowed(platform = process.platform, envValue = process.env.AIWG_COCKPIT_LOCAL_LIBVIRT_FALLBACK) {
|
|
37
39
|
return platform === 'linux' || envValue === '1';
|
|
38
40
|
}
|
|
@@ -689,6 +691,11 @@ async function getExecutorCapabilities(executorUrl) {
|
|
|
689
691
|
host_runtime_enabled: body.host_runtime_enabled === true || body.hostRuntimeEnabled === true,
|
|
690
692
|
runtime_providers: runtimeProviders && Array.isArray(runtimeProviders.providers) ? runtimeProviders : undefined,
|
|
691
693
|
raw_status: body.status ?? body.state ?? 'unknown',
|
|
694
|
+
real_executor: !mockExecutorReason(body),
|
|
695
|
+
implementation: body.implementation ?? body.service ?? body.name ?? 'agentic-sandbox',
|
|
696
|
+
version: body.version ?? body.build?.version ?? null,
|
|
697
|
+
commit: body.commit ?? body.build?.commit ?? body.git_commit ?? null,
|
|
698
|
+
auth_required: body.auth_required ?? body.auth?.required ?? null,
|
|
692
699
|
};
|
|
693
700
|
} catch (err) {
|
|
694
701
|
rethrowExecutorSecurityError(err);
|
|
@@ -1527,6 +1534,7 @@ function normalizeInstance(executorUrl, i) {
|
|
|
1527
1534
|
state: i.state ?? i.status ?? 'unknown',
|
|
1528
1535
|
tenant: i.tenant_id ?? i.tenant ?? i.tenantId ?? 'default',
|
|
1529
1536
|
card_url: i.card_url ?? i.cardUrl ?? `${executorUrl}/agents/${encodeURIComponent(id)}/.well-known/agent-card.json`,
|
|
1537
|
+
a2a_protocol: i.a2a_protocol ?? i.a2aProtocol,
|
|
1530
1538
|
runtime_posture: runtimePosture,
|
|
1531
1539
|
host_daemon: normalizeHostDaemon(i.host_daemon ?? i.hostDaemon, runtimePosture.kind),
|
|
1532
1540
|
transport: normalizeTransport(
|
|
@@ -1756,6 +1764,113 @@ function runtimeExtensionFromCard(card) {
|
|
|
1756
1764
|
return ext?.params && typeof ext.params === 'object' ? ext.params : null;
|
|
1757
1765
|
}
|
|
1758
1766
|
|
|
1767
|
+
function cockpitA2ASettings() {
|
|
1768
|
+
const context = executorRequestContext.getStore();
|
|
1769
|
+
return {
|
|
1770
|
+
policy: context?.a2aProtocolPolicy ?? COCKPIT_A2A_PROTOCOL_POLICY,
|
|
1771
|
+
allowFallback: context?.allowA2AProtocolFallback ?? COCKPIT_A2A_PROTOCOL_FALLBACK,
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
/** Normalize and select the ordered AgentCard interface Cockpit will use. */
|
|
1776
|
+
export function selectCockpitA2AInterface(card, policy = '0.3') {
|
|
1777
|
+
if (!['0.3', '1.0', 'auto'].includes(policy)) {
|
|
1778
|
+
throw new Error(`invalid Cockpit A2A protocol policy: ${policy}`);
|
|
1779
|
+
}
|
|
1780
|
+
const normalizeVersion = (value) => {
|
|
1781
|
+
const match = /^(0\.3|1\.0)(?:\.\d+)?$/.exec(String(value ?? '').trim());
|
|
1782
|
+
return match?.[1] ?? null;
|
|
1783
|
+
};
|
|
1784
|
+
const topVersion = normalizeVersion(card?.protocolVersion);
|
|
1785
|
+
const interfaces = [];
|
|
1786
|
+
for (const [preference, entry] of (Array.isArray(card?.supportedInterfaces) ? card.supportedInterfaces : []).entries()) {
|
|
1787
|
+
const version = normalizeVersion(entry?.protocolVersion) ?? topVersion;
|
|
1788
|
+
const binding = entry?.protocolBinding ?? entry?.transport;
|
|
1789
|
+
if (!version || !binding || typeof entry?.url !== 'string') continue;
|
|
1790
|
+
interfaces.push({
|
|
1791
|
+
url: entry.url.replace(/\/+$/, ''),
|
|
1792
|
+
protocol_version: version,
|
|
1793
|
+
protocol_binding: String(binding),
|
|
1794
|
+
preference,
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
if (topVersion === '0.3' && typeof card?.url === 'string' && !interfaces.some((entry) => entry.protocol_version === '0.3')) {
|
|
1798
|
+
interfaces.push({
|
|
1799
|
+
url: card.url.replace(/\/+$/, ''),
|
|
1800
|
+
protocol_version: '0.3',
|
|
1801
|
+
protocol_binding: String(card.preferredTransport ?? 'REST'),
|
|
1802
|
+
preference: interfaces.length,
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
const versions = policy === 'auto' ? ['1.0', '0.3'] : [policy];
|
|
1806
|
+
for (const version of versions) {
|
|
1807
|
+
const selected = interfaces
|
|
1808
|
+
.filter((entry) => entry.protocol_version === version)
|
|
1809
|
+
.sort((a, b) => a.preference - b.preference)[0];
|
|
1810
|
+
if (selected) return { policy, ...selected };
|
|
1811
|
+
}
|
|
1812
|
+
throw new Error(`AgentCard has no interface compatible with Cockpit A2A policy ${policy}`);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
async function discoverCockpitA2AInterface(executorUrl, instanceId) {
|
|
1816
|
+
const { body: card } = await fetchJsonFirst([
|
|
1817
|
+
`${executorUrl}/agents/${encodeURIComponent(instanceId)}/.well-known/agent-card.json`,
|
|
1818
|
+
]);
|
|
1819
|
+
return { card, selected: selectCockpitA2AInterface(card, cockpitA2ASettings().policy) };
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
function cockpitA2AHeaders(version, mutating = false) {
|
|
1823
|
+
const mediaType = version === '1.0' ? 'application/a2a+json' : 'application/json';
|
|
1824
|
+
return {
|
|
1825
|
+
accept: mediaType,
|
|
1826
|
+
...(mutating ? { 'content-type': mediaType } : {}),
|
|
1827
|
+
...(version === '1.0' ? { 'a2a-version': '1.0' } : {}),
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function isA2AVersionNotSupported(status, body) {
|
|
1832
|
+
const type = String(body?.type ?? '').toLowerCase();
|
|
1833
|
+
const code = String(body?.code ?? body?.error?.code ?? '').toLowerCase();
|
|
1834
|
+
return status === 400 && (
|
|
1835
|
+
type.includes('version-not-supported') ||
|
|
1836
|
+
['versionnotsupportederror', 'version_not_supported', 'a2a.version_not_supported', '-32009'].includes(code)
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
async function negotiatedCockpitA2ARequest(executorUrl, instanceId, candidatesFor) {
|
|
1841
|
+
const settings = cockpitA2ASettings();
|
|
1842
|
+
let card;
|
|
1843
|
+
let selected;
|
|
1844
|
+
try {
|
|
1845
|
+
({ card, selected } = await discoverCockpitA2AInterface(executorUrl, instanceId));
|
|
1846
|
+
} catch (error) {
|
|
1847
|
+
// Pre-AgentCard executors remain supported only under the explicit legacy
|
|
1848
|
+
// policy. Auto and 1.0 must negotiate from advertised interfaces.
|
|
1849
|
+
if (settings.policy !== '0.3') throw error;
|
|
1850
|
+
selected = {
|
|
1851
|
+
policy: '0.3',
|
|
1852
|
+
url: `${executorUrl}/agents/${encodeURIComponent(instanceId)}`,
|
|
1853
|
+
protocol_version: '0.3',
|
|
1854
|
+
protocol_binding: 'REST',
|
|
1855
|
+
preference: 0,
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
let result = await fetchJsonFirst(candidatesFor(selected));
|
|
1859
|
+
let active = selected;
|
|
1860
|
+
let fallbackReason;
|
|
1861
|
+
if (
|
|
1862
|
+
selected.protocol_version === '1.0' &&
|
|
1863
|
+
settings.policy === 'auto' &&
|
|
1864
|
+
settings.allowFallback &&
|
|
1865
|
+
isA2AVersionNotSupported(result.status, result.body)
|
|
1866
|
+
) {
|
|
1867
|
+
active = selectCockpitA2AInterface(card, '0.3');
|
|
1868
|
+
fallbackReason = `${result.body?.type ?? result.body?.code ?? 'VersionNotSupportedError'}`;
|
|
1869
|
+
result = await fetchJsonFirst(candidatesFor(active));
|
|
1870
|
+
}
|
|
1871
|
+
return { ...result, selected: active, fallbackReason };
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1759
1874
|
async function enrichInstanceFromAgentCard(executorUrl, instance) {
|
|
1760
1875
|
const id = instance.instance_id ?? instance.instanceId ?? instance.id;
|
|
1761
1876
|
if (!id) return instance;
|
|
@@ -1764,12 +1879,20 @@ async function enrichInstanceFromAgentCard(executorUrl, instance) {
|
|
|
1764
1879
|
`${executorUrl}/agents/${encodeURIComponent(id)}/.well-known/agent-card.json`,
|
|
1765
1880
|
]);
|
|
1766
1881
|
const runtimeExtension = runtimeExtensionFromCard(body);
|
|
1767
|
-
|
|
1882
|
+
const selected = selectCockpitA2AInterface(body, cockpitA2ASettings().policy);
|
|
1768
1883
|
return {
|
|
1769
1884
|
...instance,
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1885
|
+
a2a_protocol: {
|
|
1886
|
+
policy: selected.policy,
|
|
1887
|
+
selected_version: selected.protocol_version,
|
|
1888
|
+
protocol_binding: selected.protocol_binding,
|
|
1889
|
+
interface_url: selected.url,
|
|
1890
|
+
},
|
|
1891
|
+
...(runtimeExtension ? {
|
|
1892
|
+
runtime_extension: runtimeExtension,
|
|
1893
|
+
loadout: instance.loadout ?? runtimeExtension.loadout,
|
|
1894
|
+
image_ref: instance.image_ref ?? runtimeExtension.image_ref,
|
|
1895
|
+
} : {}),
|
|
1773
1896
|
};
|
|
1774
1897
|
} catch (err) {
|
|
1775
1898
|
rethrowExecutorSecurityError(err);
|
|
@@ -1896,20 +2019,59 @@ async function getInventory(executorUrl, { requireSandboxMtls = false } = {}) {
|
|
|
1896
2019
|
// /admin/running. A2A task lifecycle states: submitted/working/input-required are
|
|
1897
2020
|
// active; completed/canceled/failed/rejected are terminal.
|
|
1898
2021
|
const ACTIVE_TASK_STATES = new Set(['submitted', 'working', 'input-required', 'in_progress', 'running']);
|
|
1899
|
-
const
|
|
2022
|
+
const V1_TASK_STATES = {
|
|
2023
|
+
TASK_STATE_SUBMITTED: 'submitted',
|
|
2024
|
+
TASK_STATE_WORKING: 'working',
|
|
2025
|
+
TASK_STATE_COMPLETED: 'completed',
|
|
2026
|
+
TASK_STATE_FAILED: 'failed',
|
|
2027
|
+
TASK_STATE_CANCELED: 'canceled',
|
|
2028
|
+
TASK_STATE_INPUT_REQUIRED: 'input-required',
|
|
2029
|
+
TASK_STATE_REJECTED: 'rejected',
|
|
2030
|
+
TASK_STATE_AUTH_REQUIRED: 'auth-required',
|
|
2031
|
+
};
|
|
2032
|
+
const normalizeTaskState = (value) => V1_TASK_STATES[value] ?? value ?? 'unknown';
|
|
2033
|
+
const taskState = (t) => normalizeTaskState(t.status?.state ?? t.state ?? (typeof t.status === 'string' ? t.status : 'unknown'));
|
|
1900
2034
|
const taskIdOf = (t) => t.id ?? t.task_id ?? t.taskId;
|
|
1901
2035
|
const taskTenantOf = (t) => t.metadata?.tenant_id ?? t.metadata?.tenantId ?? t.tenant ?? t.tenant_id ?? t.tenantId ?? 'default';
|
|
1902
2036
|
|
|
2037
|
+
function normalizeCockpitA2ATask(task) {
|
|
2038
|
+
if (!task || typeof task !== 'object' || !task.id || !task.status || typeof task.status !== 'object') return task;
|
|
2039
|
+
return { ...task, status: { ...task.status, state: normalizeTaskState(task.status.state) } };
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
function normalizeCockpitA2ATaskResponse(body) {
|
|
2043
|
+
const candidate = body?.task && typeof body.task === 'object' ? body.task : body;
|
|
2044
|
+
return normalizeCockpitA2ATask(candidate);
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1903
2047
|
/** Active tasks for one instance via the A2A task surface (#1639). The session
|
|
1904
2048
|
* agent id (not the instance id) keys the agent routes on real executors. */
|
|
1905
2049
|
async function listInstanceTasks(executorUrl, instanceId) {
|
|
1906
2050
|
const agentId = await resolveSessionAgentId(executorUrl, instanceId);
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2051
|
+
const settings = cockpitA2ASettings();
|
|
2052
|
+
try {
|
|
2053
|
+
const result = await negotiatedCockpitA2ARequest(executorUrl, agentId, (selected) => {
|
|
2054
|
+
const headers = cockpitA2AHeaders(selected.protocol_version);
|
|
2055
|
+
return selected.protocol_version === '1.0'
|
|
2056
|
+
? [{ target: `${selected.url}/tasks`, headers }]
|
|
2057
|
+
: [
|
|
2058
|
+
{ target: `${selected.url}/v1/tasks`, headers },
|
|
2059
|
+
{ target: `${selected.url}/tasks`, headers },
|
|
2060
|
+
];
|
|
2061
|
+
});
|
|
2062
|
+
if (result.status >= 400) {
|
|
2063
|
+
throw new Error(`A2A ${result.selected.protocol_version} task list failed with HTTP ${result.status}`);
|
|
2064
|
+
}
|
|
2065
|
+
return asArrayFromEnvelope(result.body, ['tasks', 'items', 'data']).map(normalizeCockpitA2ATask);
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
if (settings.policy !== '0.3') throw error;
|
|
2068
|
+
const candidates = unique([instanceId, agentId]).flatMap((id) => [
|
|
2069
|
+
`${executorUrl}/agents/${encodeURIComponent(id)}/tasks`,
|
|
2070
|
+
`${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/tasks`,
|
|
2071
|
+
]);
|
|
2072
|
+
const { body } = await fetchJsonFirst(candidates);
|
|
2073
|
+
return asArrayFromEnvelope(body, ['tasks', 'items', 'data']).map(normalizeCockpitA2ATask);
|
|
2074
|
+
}
|
|
1913
2075
|
}
|
|
1914
2076
|
|
|
1915
2077
|
/** Running board derived from active A2A tasks across running instances (#1639).
|
|
@@ -2073,6 +2235,41 @@ function missionSummary(mission) {
|
|
|
2073
2235
|
completed_at: mission.completedAt ?? mission.completed_at,
|
|
2074
2236
|
error: mission.error,
|
|
2075
2237
|
terminal: TERMINAL_MISSION_STATES.has(status),
|
|
2238
|
+
...(graphMissionProjection(mission) ?? {}),
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
function graphMissionProjection(mission) {
|
|
2243
|
+
const graph = mission.graph ?? mission.graph_metadata;
|
|
2244
|
+
if (!graph || typeof graph !== 'object' || !graph.graphId || !graph.runId) return null;
|
|
2245
|
+
const nodes = Array.isArray(mission.graphNodes ?? mission.graph_nodes)
|
|
2246
|
+
? (mission.graphNodes ?? mission.graph_nodes).map((node) => ({
|
|
2247
|
+
node_id: node.nodeId ?? node.node_id,
|
|
2248
|
+
node_run_id: node.nodeRunId ?? node.node_run_id,
|
|
2249
|
+
state: node.state ?? node.nodeState ?? 'unknown',
|
|
2250
|
+
runtime_binding: node.runtimeBinding ?? node.runtime_binding ?? 'unknown',
|
|
2251
|
+
route_reason: node.routeReason ?? node.route_reason,
|
|
2252
|
+
evidence_summary: node.evidenceSummary ?? node.evidence_summary,
|
|
2253
|
+
hitl_status: node.hitlStatus ?? node.hitl_status,
|
|
2254
|
+
cost_usd: Number(node.costUsd ?? node.cost_usd ?? 0),
|
|
2255
|
+
tokens: Number(node.tokens ?? 0),
|
|
2256
|
+
duration_ms: Number(node.durationMs ?? node.duration_ms ?? 0),
|
|
2257
|
+
retry_count: Number(node.retryCount ?? node.retry_count ?? 0),
|
|
2258
|
+
budget_remaining: node.budgetRemaining ?? node.budget_remaining,
|
|
2259
|
+
checkpoint_id: node.checkpointId ?? node.checkpoint_id,
|
|
2260
|
+
replay_of_node_run_id: node.replayOfNodeRunId ?? node.replay_of_node_run_id,
|
|
2261
|
+
artifacts: Array.isArray(node.artifacts) ? node.artifacts : [],
|
|
2262
|
+
})) : [];
|
|
2263
|
+
return {
|
|
2264
|
+
graph: {
|
|
2265
|
+
schema_version: graph.schemaVersion ?? graph.schema_version ?? 'graph.flow.aiwg.io/v1',
|
|
2266
|
+
graph_id: graph.graphId,
|
|
2267
|
+
graph_version: graph.graphVersion,
|
|
2268
|
+
run_id: graph.runId,
|
|
2269
|
+
replay_of_run_id: graph.replayOfRunId ?? graph.replay_of_run_id,
|
|
2270
|
+
checkpoint_id: graph.checkpointId ?? graph.checkpoint_id,
|
|
2271
|
+
},
|
|
2272
|
+
graph_nodes: nodes,
|
|
2076
2273
|
};
|
|
2077
2274
|
}
|
|
2078
2275
|
|
|
@@ -2221,6 +2418,10 @@ function fleetMissionProjection(record, sessionId) {
|
|
|
2221
2418
|
exit_classification: status.exit_classification,
|
|
2222
2419
|
error: status.error_code,
|
|
2223
2420
|
schedule: record.spec?.schedule,
|
|
2421
|
+
...(graphMissionProjection({
|
|
2422
|
+
graph_metadata: record.lineage?.graph_metadata ?? record.metadata?.['aiwg.flow.graph'],
|
|
2423
|
+
graph_nodes: record.status?.graph_nodes,
|
|
2424
|
+
}) ?? {}),
|
|
2224
2425
|
};
|
|
2225
2426
|
}
|
|
2226
2427
|
|
|
@@ -2331,46 +2532,55 @@ async function respondApproval(executorUrl, approvalId, decision) {
|
|
|
2331
2532
|
const [instanceId, taskId] = String(approvalId).split('::');
|
|
2332
2533
|
if (!instanceId || !taskId) return { status: 400, body: { error: 'invalid_approval_id' } };
|
|
2333
2534
|
const agentId = await resolveSessionAgentId(executorUrl, instanceId);
|
|
2334
|
-
const message = {
|
|
2335
|
-
message: {
|
|
2336
|
-
messageId: `cockpit-hitl-${Date.now()}`,
|
|
2337
|
-
role: 'user',
|
|
2338
|
-
taskId,
|
|
2339
|
-
contextId: taskId,
|
|
2340
|
-
parts: [{ kind: 'text', text: decision }],
|
|
2341
|
-
metadata: { hitl_response: { decision }, approval_decision: decision },
|
|
2342
|
-
},
|
|
2343
|
-
};
|
|
2344
|
-
const response = JSON.stringify({ decision, response: message.message });
|
|
2345
|
-
const candidates = unique([agentId, instanceId]).flatMap((id) => [
|
|
2346
|
-
{
|
|
2347
|
-
target: `${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}:respond`,
|
|
2348
|
-
method: 'POST',
|
|
2349
|
-
headers: { 'content-type': 'application/json' },
|
|
2350
|
-
body: response,
|
|
2351
|
-
},
|
|
2352
|
-
{
|
|
2353
|
-
target: `${executorUrl}/agents/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}:respond`,
|
|
2354
|
-
method: 'POST',
|
|
2355
|
-
headers: { 'content-type': 'application/json' },
|
|
2356
|
-
body: response,
|
|
2357
|
-
},
|
|
2358
|
-
{
|
|
2359
|
-
target: `${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/messages:send`,
|
|
2360
|
-
method: 'POST',
|
|
2361
|
-
headers: { 'content-type': 'application/json' },
|
|
2362
|
-
body: JSON.stringify(message),
|
|
2363
|
-
},
|
|
2364
|
-
{
|
|
2365
|
-
target: `${executorUrl}/agents/${encodeURIComponent(id)}/messages:send`,
|
|
2366
|
-
method: 'POST',
|
|
2367
|
-
headers: { 'content-type': 'application/json' },
|
|
2368
|
-
body: JSON.stringify(message),
|
|
2369
|
-
},
|
|
2370
|
-
]);
|
|
2371
2535
|
try {
|
|
2372
|
-
const
|
|
2373
|
-
|
|
2536
|
+
const result = await negotiatedCockpitA2ARequest(executorUrl, agentId, (selected) => {
|
|
2537
|
+
const message = {
|
|
2538
|
+
messageId: `cockpit-hitl-${Date.now()}`,
|
|
2539
|
+
role: selected.protocol_version === '1.0' ? 'ROLE_USER' : 'user',
|
|
2540
|
+
taskId,
|
|
2541
|
+
contextId: taskId,
|
|
2542
|
+
parts: [selected.protocol_version === '1.0'
|
|
2543
|
+
? { text: decision }
|
|
2544
|
+
: { kind: 'text', text: decision }],
|
|
2545
|
+
metadata: { hitl_response: { decision }, approval_decision: decision },
|
|
2546
|
+
};
|
|
2547
|
+
if (selected.protocol_version === '1.0') {
|
|
2548
|
+
return [{
|
|
2549
|
+
target: `${selected.url}/message:send`,
|
|
2550
|
+
method: 'POST',
|
|
2551
|
+
headers: cockpitA2AHeaders('1.0', true),
|
|
2552
|
+
body: JSON.stringify({ message }),
|
|
2553
|
+
}];
|
|
2554
|
+
}
|
|
2555
|
+
const response = JSON.stringify({ decision, response: message });
|
|
2556
|
+
return [
|
|
2557
|
+
{
|
|
2558
|
+
target: `${selected.url}/v1/tasks/${encodeURIComponent(taskId)}:respond`,
|
|
2559
|
+
method: 'POST',
|
|
2560
|
+
headers: cockpitA2AHeaders('0.3', true),
|
|
2561
|
+
body: response,
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
target: `${selected.url}/tasks/${encodeURIComponent(taskId)}:respond`,
|
|
2565
|
+
method: 'POST',
|
|
2566
|
+
headers: cockpitA2AHeaders('0.3', true),
|
|
2567
|
+
body: response,
|
|
2568
|
+
},
|
|
2569
|
+
{
|
|
2570
|
+
target: `${executorUrl}/api/v1/agents/${encodeURIComponent(agentId)}/tasks/${encodeURIComponent(taskId)}:respond`,
|
|
2571
|
+
method: 'POST',
|
|
2572
|
+
headers: cockpitA2AHeaders('0.3', true),
|
|
2573
|
+
body: response,
|
|
2574
|
+
},
|
|
2575
|
+
{
|
|
2576
|
+
target: `${selected.url}/v1/messages:send`,
|
|
2577
|
+
method: 'POST',
|
|
2578
|
+
headers: cockpitA2AHeaders('0.3', true),
|
|
2579
|
+
body: JSON.stringify({ message }),
|
|
2580
|
+
},
|
|
2581
|
+
];
|
|
2582
|
+
});
|
|
2583
|
+
return { status: result.status, body: normalizeCockpitA2ATaskResponse(result.body) };
|
|
2374
2584
|
} catch (e) {
|
|
2375
2585
|
rethrowExecutorSecurityError(e);
|
|
2376
2586
|
return { status: 409, body: { error: 'approval_response_failed', detail: String(e?.message ?? e) } };
|
|
@@ -2656,7 +2866,12 @@ export function createBridge({
|
|
|
2656
2866
|
requireSandboxMtls = REQUIRE_SANDBOX_MTLS,
|
|
2657
2867
|
bootstrapTtlMs = 60_000,
|
|
2658
2868
|
sessionTtlMs = 12 * 60 * 60 * 1000,
|
|
2869
|
+
a2aProtocolPolicy = COCKPIT_A2A_PROTOCOL_POLICY,
|
|
2870
|
+
allowA2AProtocolFallback = COCKPIT_A2A_PROTOCOL_FALLBACK,
|
|
2659
2871
|
} = {}) {
|
|
2872
|
+
if (!['0.3', '1.0', 'auto'].includes(a2aProtocolPolicy)) {
|
|
2873
|
+
throw new Error(`AIWG_COCKPIT_A2A_PROTOCOL_POLICY must be 0.3, 1.0, or auto (received '${a2aProtocolPolicy}')`);
|
|
2874
|
+
}
|
|
2660
2875
|
const upstreamUrl = executorUrl;
|
|
2661
2876
|
const TOKEN = token ?? randomBytes(24).toString('hex');
|
|
2662
2877
|
const bootstrapNonces = new Map();
|
|
@@ -3190,8 +3405,33 @@ export function createBridge({
|
|
|
3190
3405
|
return json(res, 200, { ...result, projection: await getMissions(upstreamUrl) });
|
|
3191
3406
|
}
|
|
3192
3407
|
if ((m = url.pathname.match(/^\/api\/tasks\/([^/]+)\/([^/]+)\/cancel$/)) && req.method === 'POST') {
|
|
3193
|
-
|
|
3194
|
-
|
|
3408
|
+
const instanceId = decodeURIComponent(m[1]);
|
|
3409
|
+
const taskId = decodeURIComponent(m[2]);
|
|
3410
|
+
await appendAudit('task.cancel.requested', { instance_id: instanceId, task_id: taskId });
|
|
3411
|
+
const agentId = await resolveSessionAgentId(upstreamUrl, instanceId);
|
|
3412
|
+
const result = await negotiatedCockpitA2ARequest(upstreamUrl, agentId, (selected) => {
|
|
3413
|
+
const request = (target) => ({
|
|
3414
|
+
target,
|
|
3415
|
+
method: 'POST',
|
|
3416
|
+
headers: cockpitA2AHeaders(selected.protocol_version, true),
|
|
3417
|
+
body: '{}',
|
|
3418
|
+
});
|
|
3419
|
+
return selected.protocol_version === '1.0'
|
|
3420
|
+
? [request(`${selected.url}/tasks/${encodeURIComponent(taskId)}:cancel`)]
|
|
3421
|
+
: [
|
|
3422
|
+
request(`${selected.url}/v1/tasks/${encodeURIComponent(taskId)}/cancel`),
|
|
3423
|
+
request(`${selected.url}/tasks/${encodeURIComponent(taskId)}/cancel`),
|
|
3424
|
+
request(`${upstreamUrl}/api/v1/agents/${encodeURIComponent(agentId)}/tasks/${encodeURIComponent(taskId)}/cancel`),
|
|
3425
|
+
];
|
|
3426
|
+
});
|
|
3427
|
+
await appendAudit('task.cancel.protocol', {
|
|
3428
|
+
instance_id: instanceId,
|
|
3429
|
+
task_id: taskId,
|
|
3430
|
+
selected_version: result.selected.protocol_version,
|
|
3431
|
+
protocol_binding: result.selected.protocol_binding,
|
|
3432
|
+
...(result.fallbackReason ? { fallback_reason: result.fallbackReason } : {}),
|
|
3433
|
+
});
|
|
3434
|
+
return json(res, result.status, normalizeCockpitA2ATaskResponse(result.body));
|
|
3195
3435
|
}
|
|
3196
3436
|
|
|
3197
3437
|
// --- approval inbox (UC-009) + cost (UC-010) ---
|
|
@@ -3212,6 +3452,11 @@ export function createBridge({
|
|
|
3212
3452
|
executor_url: upstreamUrl,
|
|
3213
3453
|
mock_executor_allowed: allowMockExecutor,
|
|
3214
3454
|
executor_auth_configured: Boolean(executorTokenFile),
|
|
3455
|
+
a2a_protocol: {
|
|
3456
|
+
policy: a2aProtocolPolicy,
|
|
3457
|
+
fallback_enabled: Boolean(allowA2AProtocolFallback),
|
|
3458
|
+
},
|
|
3459
|
+
executor: await getExecutorCapabilities(upstreamUrl),
|
|
3215
3460
|
});
|
|
3216
3461
|
if (url.pathname === '/' || url.pathname === '/index.html') {
|
|
3217
3462
|
const distIndex = join(WEB_DIST, 'index.html');
|
|
@@ -3240,11 +3485,11 @@ export function createBridge({
|
|
|
3240
3485
|
}
|
|
3241
3486
|
};
|
|
3242
3487
|
const server = http.createServer((req, res) => executorRequestContext.run(
|
|
3243
|
-
{ executorOrigin, executorTokenFile },
|
|
3488
|
+
{ executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback },
|
|
3244
3489
|
() => handleRequest(req, res),
|
|
3245
3490
|
));
|
|
3246
3491
|
server.on('upgrade', (req, socket, head) => executorRequestContext.run(
|
|
3247
|
-
{ executorOrigin, executorTokenFile },
|
|
3492
|
+
{ executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback },
|
|
3248
3493
|
async () => {
|
|
3249
3494
|
try {
|
|
3250
3495
|
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
|
package/bridge/src/smoke.mjs
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { createExecutor, DEFAULT_INSTANCE } from '../../mock-executor/src/server.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
createBridge,
|
|
8
|
+
localLibvirtFallbackAllowed,
|
|
9
|
+
normalizeSessionRows,
|
|
10
|
+
selectCockpitA2AInterface,
|
|
11
|
+
} from './server.mjs';
|
|
7
12
|
|
|
8
13
|
const mock = createExecutor();
|
|
9
14
|
await new Promise((r) => mock.listen(0, '127.0.0.1', r));
|
|
@@ -37,6 +42,17 @@ try {
|
|
|
37
42
|
assert.equal(i0.storage?.persistent, true, 'storage persistence surfaced');
|
|
38
43
|
assert.equal(i0.storage?.delete_on_destroy, true, 'storage delete-on-destroy surfaced');
|
|
39
44
|
assert.ok(['vm', 'container', 'host', 'wasm-edge'].includes(i0.runtime), 'runtime kind');
|
|
45
|
+
assert.equal(i0.a2a_protocol?.selected_version, '0.3', 'inventory exposes the selected A2A protocol');
|
|
46
|
+
assert.equal(selectCockpitA2AInterface({
|
|
47
|
+
name: 'dual',
|
|
48
|
+
version: '1',
|
|
49
|
+
protocolVersion: '0.3.0',
|
|
50
|
+
url: 'https://legacy.test/agent',
|
|
51
|
+
supportedInterfaces: [
|
|
52
|
+
{ url: 'https://v1.test/agent', protocolBinding: 'HTTP+JSON', protocolVersion: '1.0' },
|
|
53
|
+
{ url: 'https://legacy.test/agent', transport: 'REST', protocolVersion: '0.3' },
|
|
54
|
+
],
|
|
55
|
+
}, 'auto').protocol_version, '1.0', 'Cockpit auto selects the preferred 1.0 interface');
|
|
40
56
|
|
|
41
57
|
// A transient executor outage must not poison Bridge state or require a
|
|
42
58
|
// Bridge restart. Every poll is a fresh upstream request, so the same Bridge
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwg/cockpit",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.18",
|
|
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",
|