@aiwg/cockpit 2026.8.17 → 2026.8.19

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
  }
@@ -1532,6 +1534,7 @@ function normalizeInstance(executorUrl, i) {
1532
1534
  state: i.state ?? i.status ?? 'unknown',
1533
1535
  tenant: i.tenant_id ?? i.tenant ?? i.tenantId ?? 'default',
1534
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,
1535
1538
  runtime_posture: runtimePosture,
1536
1539
  host_daemon: normalizeHostDaemon(i.host_daemon ?? i.hostDaemon, runtimePosture.kind),
1537
1540
  transport: normalizeTransport(
@@ -1761,6 +1764,113 @@ function runtimeExtensionFromCard(card) {
1761
1764
  return ext?.params && typeof ext.params === 'object' ? ext.params : null;
1762
1765
  }
1763
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
+
1764
1874
  async function enrichInstanceFromAgentCard(executorUrl, instance) {
1765
1875
  const id = instance.instance_id ?? instance.instanceId ?? instance.id;
1766
1876
  if (!id) return instance;
@@ -1769,12 +1879,20 @@ async function enrichInstanceFromAgentCard(executorUrl, instance) {
1769
1879
  `${executorUrl}/agents/${encodeURIComponent(id)}/.well-known/agent-card.json`,
1770
1880
  ]);
1771
1881
  const runtimeExtension = runtimeExtensionFromCard(body);
1772
- if (!runtimeExtension) return instance;
1882
+ const selected = selectCockpitA2AInterface(body, cockpitA2ASettings().policy);
1773
1883
  return {
1774
1884
  ...instance,
1775
- runtime_extension: runtimeExtension,
1776
- loadout: instance.loadout ?? runtimeExtension.loadout,
1777
- 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
+ } : {}),
1778
1896
  };
1779
1897
  } catch (err) {
1780
1898
  rethrowExecutorSecurityError(err);
@@ -1901,20 +2019,59 @@ async function getInventory(executorUrl, { requireSandboxMtls = false } = {}) {
1901
2019
  // /admin/running. A2A task lifecycle states: submitted/working/input-required are
1902
2020
  // active; completed/canceled/failed/rejected are terminal.
1903
2021
  const ACTIVE_TASK_STATES = new Set(['submitted', 'working', 'input-required', 'in_progress', 'running']);
1904
- 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'));
1905
2034
  const taskIdOf = (t) => t.id ?? t.task_id ?? t.taskId;
1906
2035
  const taskTenantOf = (t) => t.metadata?.tenant_id ?? t.metadata?.tenantId ?? t.tenant ?? t.tenant_id ?? t.tenantId ?? 'default';
1907
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
+
1908
2047
  /** Active tasks for one instance via the A2A task surface (#1639). The session
1909
2048
  * agent id (not the instance id) keys the agent routes on real executors. */
1910
2049
  async function listInstanceTasks(executorUrl, instanceId) {
1911
2050
  const agentId = await resolveSessionAgentId(executorUrl, instanceId);
1912
- const candidates = unique([instanceId, agentId]).flatMap((id) => [
1913
- `${executorUrl}/agents/${encodeURIComponent(id)}/tasks`,
1914
- `${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/tasks`,
1915
- ]);
1916
- const { body } = await fetchJsonFirst(candidates);
1917
- 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
+ }
1918
2075
  }
1919
2076
 
1920
2077
  /** Running board derived from active A2A tasks across running instances (#1639).
@@ -2078,6 +2235,41 @@ function missionSummary(mission) {
2078
2235
  completed_at: mission.completedAt ?? mission.completed_at,
2079
2236
  error: mission.error,
2080
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,
2081
2273
  };
2082
2274
  }
2083
2275
 
@@ -2226,6 +2418,10 @@ function fleetMissionProjection(record, sessionId) {
2226
2418
  exit_classification: status.exit_classification,
2227
2419
  error: status.error_code,
2228
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
+ }) ?? {}),
2229
2425
  };
2230
2426
  }
2231
2427
 
@@ -2336,46 +2532,55 @@ async function respondApproval(executorUrl, approvalId, decision) {
2336
2532
  const [instanceId, taskId] = String(approvalId).split('::');
2337
2533
  if (!instanceId || !taskId) return { status: 400, body: { error: 'invalid_approval_id' } };
2338
2534
  const agentId = await resolveSessionAgentId(executorUrl, instanceId);
2339
- const message = {
2340
- message: {
2341
- messageId: `cockpit-hitl-${Date.now()}`,
2342
- role: 'user',
2343
- taskId,
2344
- contextId: taskId,
2345
- parts: [{ kind: 'text', text: decision }],
2346
- metadata: { hitl_response: { decision }, approval_decision: decision },
2347
- },
2348
- };
2349
- const response = JSON.stringify({ decision, response: message.message });
2350
- const candidates = unique([agentId, instanceId]).flatMap((id) => [
2351
- {
2352
- target: `${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}:respond`,
2353
- method: 'POST',
2354
- headers: { 'content-type': 'application/json' },
2355
- body: response,
2356
- },
2357
- {
2358
- target: `${executorUrl}/agents/${encodeURIComponent(id)}/tasks/${encodeURIComponent(taskId)}:respond`,
2359
- method: 'POST',
2360
- headers: { 'content-type': 'application/json' },
2361
- body: response,
2362
- },
2363
- {
2364
- target: `${executorUrl}/api/v1/agents/${encodeURIComponent(id)}/messages:send`,
2365
- method: 'POST',
2366
- headers: { 'content-type': 'application/json' },
2367
- body: JSON.stringify(message),
2368
- },
2369
- {
2370
- target: `${executorUrl}/agents/${encodeURIComponent(id)}/messages:send`,
2371
- method: 'POST',
2372
- headers: { 'content-type': 'application/json' },
2373
- body: JSON.stringify(message),
2374
- },
2375
- ]);
2376
2535
  try {
2377
- const { status, body } = await fetchJsonFirst(candidates);
2378
- 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) };
2379
2584
  } catch (e) {
2380
2585
  rethrowExecutorSecurityError(e);
2381
2586
  return { status: 409, body: { error: 'approval_response_failed', detail: String(e?.message ?? e) } };
@@ -2661,7 +2866,12 @@ export function createBridge({
2661
2866
  requireSandboxMtls = REQUIRE_SANDBOX_MTLS,
2662
2867
  bootstrapTtlMs = 60_000,
2663
2868
  sessionTtlMs = 12 * 60 * 60 * 1000,
2869
+ a2aProtocolPolicy = COCKPIT_A2A_PROTOCOL_POLICY,
2870
+ allowA2AProtocolFallback = COCKPIT_A2A_PROTOCOL_FALLBACK,
2664
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
+ }
2665
2875
  const upstreamUrl = executorUrl;
2666
2876
  const TOKEN = token ?? randomBytes(24).toString('hex');
2667
2877
  const bootstrapNonces = new Map();
@@ -3195,8 +3405,33 @@ export function createBridge({
3195
3405
  return json(res, 200, { ...result, projection: await getMissions(upstreamUrl) });
3196
3406
  }
3197
3407
  if ((m = url.pathname.match(/^\/api\/tasks\/([^/]+)\/([^/]+)\/cancel$/)) && req.method === 'POST') {
3198
- await appendAudit('task.cancel.requested', { instance_id: decodeURIComponent(m[1]), task_id: decodeURIComponent(m[2]) });
3199
- 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));
3200
3435
  }
3201
3436
 
3202
3437
  // --- approval inbox (UC-009) + cost (UC-010) ---
@@ -3217,6 +3452,10 @@ export function createBridge({
3217
3452
  executor_url: upstreamUrl,
3218
3453
  mock_executor_allowed: allowMockExecutor,
3219
3454
  executor_auth_configured: Boolean(executorTokenFile),
3455
+ a2a_protocol: {
3456
+ policy: a2aProtocolPolicy,
3457
+ fallback_enabled: Boolean(allowA2AProtocolFallback),
3458
+ },
3220
3459
  executor: await getExecutorCapabilities(upstreamUrl),
3221
3460
  });
3222
3461
  if (url.pathname === '/' || url.pathname === '/index.html') {
@@ -3246,11 +3485,11 @@ export function createBridge({
3246
3485
  }
3247
3486
  };
3248
3487
  const server = http.createServer((req, res) => executorRequestContext.run(
3249
- { executorOrigin, executorTokenFile },
3488
+ { executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback },
3250
3489
  () => handleRequest(req, res),
3251
3490
  ));
3252
3491
  server.on('upgrade', (req, socket, head) => executorRequestContext.run(
3253
- { executorOrigin, executorTokenFile },
3492
+ { executorOrigin, executorTokenFile, a2aProtocolPolicy, allowA2AProtocolFallback },
3254
3493
  async () => {
3255
3494
  try {
3256
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.17",
3
+ "version": "2026.8.19",
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",