@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 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
@@ -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
- if (!runtimeExtension) return instance;
1882
+ const selected = selectCockpitA2AInterface(body, cockpitA2ASettings().policy);
1768
1883
  return {
1769
1884
  ...instance,
1770
- runtime_extension: runtimeExtension,
1771
- loadout: instance.loadout ?? runtimeExtension.loadout,
1772
- image_ref: instance.image_ref ?? runtimeExtension.image_ref,
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 taskState = (t) => t.status?.state ?? t.state ?? (typeof t.status === 'string' ? t.status : 'unknown');
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 candidates = unique([instanceId, agentId]).flatMap((id) => [
1908
- `${executorUrl}/agents/${encodeURIComponent(id)}/tasks`,
1909
- `${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/tasks`,
1910
- ]);
1911
- const { body } = await fetchJsonFirst(candidates);
1912
- return asArrayFromEnvelope(body, ['tasks', 'items', 'data']);
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 { status, body } = await fetchJsonFirst(candidates);
2373
- return { status, body };
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
- await appendAudit('task.cancel.requested', { instance_id: decodeURIComponent(m[1]), task_id: decodeURIComponent(m[2]) });
3194
- return proxy(res, 'POST', `${upstreamUrl}/agents/${encodeURIComponent(m[1])}/tasks/${encodeURIComponent(m[2])}:cancel`);
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'}`);
@@ -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 { createBridge, localLibvirtFallbackAllowed, normalizeSessionRows } from './server.mjs';
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.16",
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",