@yeaft/webchat-agent 1.0.590 → 1.0.591

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.
@@ -19,7 +19,7 @@ import {
19
19
  import {
20
20
  sendToWebClient, forwardToAgent,
21
21
  broadcastAgentList, broadcastSessionCatalog, buildSessionCatalog, buildHiddenSessionCatalog,
22
- verifyConversationOwnership, verifyAgentOwnership
22
+ verifyConversationOwnership, verifyAgentOwnership, resolveAgentAccessError
23
23
  } from '../ws-utils.js';
24
24
  import { routeSessionPin } from './session-pin-router.js';
25
25
  import { recordPerfTraceEvent } from '../perf-trace.js';
@@ -87,6 +87,26 @@ async function sendAskUserAgentUnavailable(client, msg, agentId) {
87
87
  });
88
88
  }
89
89
 
90
+ async function sendYeaftSessionSendError(client, msg, agentId, code = 'agent_unavailable') {
91
+ // Keep the top-level error contract understood by existing Web clients.
92
+ // Correlation identifies the rejected send, not an Agent acknowledgement.
93
+ await sendToWebClient(client, {
94
+ type: 'error',
95
+ agentId: agentId || null,
96
+ sessionId: msg.sessionId ?? null,
97
+ conversationId: msg.conversationId ?? null,
98
+ requestId: msg.requestId ?? null,
99
+ id: msg.id ?? null,
100
+ clientMessageId: msg.id ?? null,
101
+ code,
102
+ message: code === 'invalid_attachment'
103
+ ? 'Session message was not sent: an attachment is missing, expired, invalid, or unavailable. Please upload the files again.'
104
+ : code === 'agent_access_denied'
105
+ ? 'Agent access denied; Session message was not sent.'
106
+ : 'No agent available; Session message was not sent. Please retry.',
107
+ });
108
+ }
109
+
90
110
  async function sendVpSnapshotError(client, msg, error) {
91
111
  await sendToWebClient(client, {
92
112
  type: 'yeaft_output',
@@ -1669,6 +1689,7 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1669
1689
  if (typeof msg.type === 'string' && (msg.type.startsWith('yeaft_') || msg.type.startsWith('unify_'))) {
1670
1690
  const relayType = msg.type.startsWith('unify_') ? `yeaft_${msg.type.slice('unify_'.length)}` : msg.type;
1671
1691
  const relayAgentId = msg.agentId || client.currentAgent;
1692
+ const isSessionSend = relayType === 'yeaft_session_send' || relayType === 'yeaft_session_chat';
1672
1693
  if (msg.perfTraceId) {
1673
1694
  recordPerfTraceEvent({
1674
1695
  traceId: msg.perfTraceId,
@@ -1682,7 +1703,9 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1682
1703
  });
1683
1704
  }
1684
1705
  if (!relayAgentId) {
1685
- if (relayType === 'yeaft_ask_user_answer') {
1706
+ if (isSessionSend) {
1707
+ await sendYeaftSessionSendError(client, msg, relayAgentId);
1708
+ } else if (relayType === 'yeaft_ask_user_answer') {
1686
1709
  await sendAskUserAgentUnavailable(client, msg, relayAgentId);
1687
1710
  } else if (relayType === 'yeaft_fetch_tool_stats') {
1688
1711
  await sendToWebClient(client, emptyYeaftToolStats('No agent selected.'));
@@ -1698,8 +1721,16 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1698
1721
  }
1699
1722
  return true; // swallow silently for legacy fire-and-forget messages
1700
1723
  }
1701
- if (!await checkAgentAccess(relayAgentId)) {
1702
- if (relayType === 'yeaft_ask_user_answer') {
1724
+ // The shared checker emits an uncorrelated error. Session sends use the
1725
+ // same access policy directly so rejection emits exactly one error.
1726
+ const sessionAccessError = isSessionSend
1727
+ ? resolveAgentAccessError(relayAgentId, client.userId, client.role)
1728
+ : null;
1729
+ if (isSessionSend ? sessionAccessError : !await checkAgentAccess(relayAgentId)) {
1730
+ if (isSessionSend) {
1731
+ await sendYeaftSessionSendError(client, msg, relayAgentId,
1732
+ sessionAccessError === 'Agent access denied' ? 'agent_access_denied' : 'agent_unavailable');
1733
+ } else if (relayType === 'yeaft_ask_user_answer') {
1703
1734
  await sendAskUserAgentUnavailable(client, msg, relayAgentId);
1704
1735
  } else if (relayType === 'yeaft_fetch_tool_stats') {
1705
1736
  await sendToWebClient(client, emptyYeaftToolStats('Agent is not available.'));
@@ -1748,6 +1779,10 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1748
1779
  }
1749
1780
  const relayAgent = agents.get(relayAgentId);
1750
1781
  if (!relayAgent || relayAgent.ws?.readyState !== 1) {
1782
+ if (isSessionSend) {
1783
+ await sendYeaftSessionSendError(client, msg, relayAgentId);
1784
+ return true;
1785
+ }
1751
1786
  if (relayType === 'yeaft_ask_user_answer') {
1752
1787
  await sendAskUserAgentUnavailable(client, msg, relayAgentId);
1753
1788
  return true;
@@ -1810,9 +1845,28 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1810
1845
  rest.id = `u_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
1811
1846
  }
1812
1847
 
1813
- // Resolve attachment fileIds → base64 BEFORE forwarding, mirroring
1814
- // The agent never sees fileIds — it
1815
- // only handles `files: [{ name, mimeType, data, isImage }]`.
1848
+ // Validate the entire Session upload set before resolving or consuming
1849
+ // any item. An invalid reference must never downgrade a send to text-only.
1850
+ if (isSessionSend && rest.attachments !== undefined) {
1851
+ const now = Date.now();
1852
+ const valid = Array.isArray(rest.attachments) && rest.attachments.every(att => {
1853
+ if (typeof att?.fileId !== 'string' || !att.fileId.trim()) return false;
1854
+ const file = pendingFiles.get(att.fileId);
1855
+ return file && Buffer.isBuffer(file.buffer)
1856
+ && typeof file.mimeType === 'string'
1857
+ && Number.isFinite(file.uploadedAt)
1858
+ && now - file.uploadedAt <= CONFIG.fileCleanupInterval
1859
+ && (CONFIG.skipAuth || (file.userId && file.userId === client.userId));
1860
+ });
1861
+ if (!valid) {
1862
+ await sendYeaftSessionSendError(client, msg, relayAgentId, 'invalid_attachment');
1863
+ return true;
1864
+ }
1865
+ }
1866
+
1867
+ // The Agent sees resolved bytes, never upload references. Session files
1868
+ // remain retryable until local dispatch succeeds (not an Agent ack).
1869
+ const sessionFileIds = new Set();
1816
1870
  if (Array.isArray(rest.attachments) && rest.attachments.length > 0) {
1817
1871
  const resolvedFiles = [];
1818
1872
  for (const att of rest.attachments) {
@@ -1825,7 +1879,8 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1825
1879
  data: file.buffer.toString('base64'),
1826
1880
  isImage: !!att.isImage || (file.mimeType || '').startsWith('image/'),
1827
1881
  });
1828
- pendingFiles.delete(att.fileId);
1882
+ if (isSessionSend) sessionFileIds.add(att.fileId);
1883
+ else pendingFiles.delete(att.fileId);
1829
1884
  } else if (file && file.userId !== client.userId) {
1830
1885
  console.warn(`[Security] User ${client.userId} attempted to use yeaft file ${att.fileId} owned by ${file.userId}`);
1831
1886
  }
@@ -1833,8 +1888,6 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1833
1888
  if (resolvedFiles.length > 0) {
1834
1889
  rest.files = resolvedFiles;
1835
1890
  }
1836
- // Drop the fileId-bearing array so the agent only sees the
1837
- // resolved form on `files`.
1838
1891
  delete rest.attachments;
1839
1892
  }
1840
1893
 
@@ -1851,7 +1904,19 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
1851
1904
  bytes: Buffer.byteLength(JSON.stringify(rest)),
1852
1905
  });
1853
1906
  }
1854
- await forwardToAgent(relayAgentId, rest);
1907
+ if (isSessionSend) {
1908
+ let dispatched = false;
1909
+ try {
1910
+ dispatched = await forwardToAgent(relayAgentId, rest);
1911
+ } catch { /* Preserve uploads and report a correlated failure below. */ }
1912
+ if (!dispatched) {
1913
+ await sendYeaftSessionSendError(client, msg, relayAgentId);
1914
+ } else {
1915
+ for (const fileId of sessionFileIds) pendingFiles.delete(fileId);
1916
+ }
1917
+ } else {
1918
+ await forwardToAgent(relayAgentId, rest);
1919
+ }
1855
1920
  return true;
1856
1921
  }
1857
1922
  return false; // Not handled
@@ -1 +1 @@
1
- {"version":"1.0.590"}
1
+ {"version":"1.0.591"}