fix(agentworld): explain missing DM key bundle (#3919)

Co-authored-by: MackJack023 <141124084+MackJack023@users.noreply.github.com>
This commit is contained in:
Felix
2026-06-22 10:15:06 -07:00
committed by GitHub
co-authored by MackJack023
parent bc49ef3e60
commit bd3e3eab00
16 changed files with 127 additions and 6 deletions
@@ -279,6 +279,76 @@ describe('DMs panel (E2E enabled)', () => {
// The messages namespace has no 'send' method — only signal.sendMessage is callable
});
test('shows a friendly error when the recipient has no published key bundle', async () => {
const user = userEvent.setup();
vi.mocked(apiClient.messages.list).mockResolvedValue({ messages: [] });
vi.mocked(apiClient.signal.sendMessage).mockRejectedValueOnce(
new Error(
'CoreRpcError: HTTP 404: HTTP 404: /keys/61KcG5aGLqpnJz2fn4tujFKAdzqsdGR9XqiUeVoT3vPg/bundle: not found'
)
);
render(<MessagingSection />);
const peerInput = screen.getByPlaceholderText(/Recipient @handle/);
await user.type(peerInput, 'peer456');
await user.click(screen.getByRole('button', { name: 'Open DM' }));
const composeInput = await screen.findByPlaceholderText(/Type a message/);
await user.type(composeInput, 'secret');
await user.click(screen.getByRole('button', { name: 'Send' }));
expect(
await screen.findByText(/This user hasn't enabled encrypted messaging yet/)
).toBeInTheDocument();
expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument();
expect(screen.queryByText(/\/keys\//)).not.toBeInTheDocument();
});
test('normalizes the core mapped missing encrypted messaging setup error', async () => {
const user = userEvent.setup();
vi.mocked(apiClient.messages.list).mockResolvedValue({ messages: [] });
vi.mocked(apiClient.signal.sendMessage).mockRejectedValueOnce(
new Error(
'CoreRpcError: Recipient has not set up encrypted messaging yet. They need to provision Signal keys before receiving DMs. (resolved agent_id: resolved-crypto-id)'
)
);
render(<MessagingSection />);
const peerInput = screen.getByPlaceholderText(/Recipient @handle/);
await user.type(peerInput, 'peer456');
await user.click(screen.getByRole('button', { name: 'Open DM' }));
const composeInput = await screen.findByPlaceholderText(/Type a message/);
await user.type(composeInput, 'secret');
await user.click(screen.getByRole('button', { name: 'Send' }));
expect(
await screen.findByText(/This user hasn't enabled encrypted messaging yet/)
).toBeInTheDocument();
expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument();
expect(screen.queryByText(/resolved agent_id/)).not.toBeInTheDocument();
});
test('normalizes missing key bundle errors while refreshing direct messages', async () => {
const user = userEvent.setup();
vi.mocked(apiClient.messages.list).mockRejectedValueOnce(
new Error(
'CoreRpcError: HTTP 404: HTTP 404: /keys/61KcG5aGLqpnJz2fn4tujFKAdzqsdGR9XqiUeVoT3vPg/bundle: not found'
)
);
render(<MessagingSection />);
const peerInput = screen.getByPlaceholderText(/Recipient @handle/);
await user.type(peerInput, 'peer456');
await user.click(screen.getByRole('button', { name: 'Open DM' }));
expect(
await screen.findByText(/This user hasn't enabled encrypted messaging yet/)
).toBeInTheDocument();
expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument();
expect(screen.queryByText(/\/keys\//)).not.toBeInTheDocument();
});
test('received messages are decrypted before display', async () => {
// directory.resolve returns 'resolved-crypto-id'; messages must use that id
// in the 'from' field to survive the peerId filter in useDirectMessages.
+29 -6
View File
@@ -27,6 +27,7 @@ import {
PaymentRequiredError,
type SignalKeyStatus,
} from '../../lib/agentworld/invokeApiClient';
import { useT } from '../../lib/i18n/I18nContext';
import { fetchWalletStatus } from '../../services/walletApi';
import { apiClient } from '../AgentWorldShell';
import { useTinyplaceStream } from '../hooks/useTinyplaceStream';
@@ -1049,7 +1050,21 @@ interface DecryptedMessage {
outgoing?: boolean;
}
function useDirectMessages(peerId: string) {
function formatDirectMessageError(err: unknown, missingBundleMessage: string): string {
const message = String(err);
const rawBundle404 = /http\s*404/i.test(message) && /\/keys\/[^/\s]+\/bundle\b/i.test(message);
const mappedMissingBundle =
/recipient/i.test(message) &&
/not\s+set\s+up/i.test(message) &&
/encrypted\s+messaging/i.test(message);
if (rawBundle404 || mappedMissingBundle) {
log('[agentworld:dm] recipient key bundle missing');
return missingBundleMessage;
}
return message;
}
function useDirectMessages(peerId: string, missingBundleMessage: string) {
const [messages, setMessages] = useState<DecryptedMessage[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -1086,11 +1101,11 @@ function useDirectMessages(peerId: string) {
return merged.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
});
} catch (err) {
setError(String(err));
setError(formatDirectMessageError(err, missingBundleMessage));
} finally {
setLoading(false);
}
}, [peerId]);
}, [missingBundleMessage, peerId]);
const send = useCallback(
async (plaintext: string) => {
@@ -1113,12 +1128,12 @@ function useDirectMessages(peerId: string) {
);
await refresh();
} catch (err) {
setError(String(err));
setError(formatDirectMessageError(err, missingBundleMessage));
} finally {
setSending(false);
}
},
[peerId, refresh]
[missingBundleMessage, peerId, refresh]
);
useEffect(() => {
@@ -1139,7 +1154,15 @@ function ActiveDmView({
composeText: string;
setComposeText: (v: string) => void;
}) {
const { messages, loading, error, sending, send } = useDirectMessages(peerId);
const { t } = useT();
const missingBundleMessage = t(
'agentworld.messaging.missingSignalBundle',
"This user hasn't enabled encrypted messaging yet. Ask them to open Agent World and enable secure DMs before sending a message."
);
const { messages, loading, error, sending, send } = useDirectMessages(
peerId,
missingBundleMessage
);
const handleSend = useCallback(async () => {
if (!composeText.trim()) return;
+2
View File
@@ -5774,6 +5774,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'إلغاء',
'agentworld.jobs.applyModal.submit': 'إرسال الطلب',
'agentworld.jobs.applyModal.submitting': 'جارٍ التقديم…',
'agentworld.messaging.missingSignalBundle':
'لم يفعّل هذا المستخدم الرسائل المشفرة بعد. اطلب منه فتح Agent World وتفعيل الرسائل المباشرة الآمنة قبل إرسال رسالة.',
};
export default messages;
+2
View File
@@ -5891,6 +5891,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'বাতিল',
'agentworld.jobs.applyModal.submit': 'আবেদন জমা দিন',
'agentworld.jobs.applyModal.submitting': 'আবেদন করা হচ্ছে…',
'agentworld.messaging.missingSignalBundle':
'এই ব্যবহারকারী এখনো এনক্রিপ্টেড মেসেজিং চালু করেননি। বার্তা পাঠানোর আগে তাকে Agent World খুলে নিরাপদ DM চালু করতে বলুন।',
};
export default messages;
+2
View File
@@ -6055,6 +6055,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Abbrechen',
'agentworld.jobs.applyModal.submit': 'Bewerbung einreichen',
'agentworld.jobs.applyModal.submitting': 'Wird eingereicht…',
'agentworld.messaging.missingSignalBundle':
'Dieser Benutzer hat verschlüsselte Nachrichten noch nicht aktiviert. Bitte ihn, Agent World zu öffnen und sichere DMs zu aktivieren, bevor du eine Nachricht sendest.',
};
export default messages;
+2
View File
@@ -6160,6 +6160,8 @@ const en: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Cancel',
'agentworld.jobs.applyModal.submit': 'Submit Application',
'agentworld.jobs.applyModal.submitting': 'Applying…',
'agentworld.messaging.missingSignalBundle':
"This user hasn't enabled encrypted messaging yet. Ask them to open Agent World and enable secure DMs before sending a message.",
};
export default en;
+2
View File
@@ -6015,6 +6015,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Cancelar',
'agentworld.jobs.applyModal.submit': 'Enviar solicitud',
'agentworld.jobs.applyModal.submitting': 'Enviando…',
'agentworld.messaging.missingSignalBundle':
'Este usuario aún no ha activado la mensajería cifrada. Pídele que abra Agent World y active los DM seguros antes de enviarle un mensaje.',
};
export default messages;
+2
View File
@@ -6033,6 +6033,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Annuler',
'agentworld.jobs.applyModal.submit': 'Soumettre la candidature',
'agentworld.jobs.applyModal.submitting': 'Envoi en cours…',
'agentworld.messaging.missingSignalBundle':
"Cet utilisateur n'a pas encore activé la messagerie chiffrée. Demandez-lui d'ouvrir Agent World et d'activer les messages privés sécurisés avant d'envoyer un message.",
};
export default messages;
+2
View File
@@ -5893,6 +5893,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'रद्द करें',
'agentworld.jobs.applyModal.submit': 'आवेदन सबमिट करें',
'agentworld.jobs.applyModal.submitting': 'आवेदन हो रहा है…',
'agentworld.messaging.missingSignalBundle':
'इस उपयोगकर्ता ने अभी तक एन्क्रिप्टेड मैसेजिंग चालू नहीं की है। संदेश भेजने से पहले उनसे Agent World खोलकर सुरक्षित DM चालू करने को कहें।',
};
export default messages;
+2
View File
@@ -5909,6 +5909,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Batal',
'agentworld.jobs.applyModal.submit': 'Kirim Lamaran',
'agentworld.jobs.applyModal.submitting': 'Melamar…',
'agentworld.messaging.missingSignalBundle':
'Pengguna ini belum mengaktifkan pesan terenkripsi. Minta mereka membuka Agent World dan mengaktifkan DM aman sebelum Anda mengirim pesan.',
};
export default messages;
+2
View File
@@ -6002,6 +6002,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Annulla',
'agentworld.jobs.applyModal.submit': 'Invia candidatura',
'agentworld.jobs.applyModal.submitting': 'Candidatura in corso…',
'agentworld.messaging.missingSignalBundle':
'Questo utente non ha ancora attivato la messaggistica crittografata. Chiedigli di aprire Agent World e attivare i DM sicuri prima di inviare un messaggio.',
};
export default messages;
+2
View File
@@ -5838,6 +5838,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': '취소',
'agentworld.jobs.applyModal.submit': '지원서 제출',
'agentworld.jobs.applyModal.submitting': '지원 중…',
'agentworld.messaging.missingSignalBundle':
'이 사용자는 아직 암호화 메시지를 활성화하지 않았습니다. 메시지를 보내기 전에 Agent World를 열고 보안 DM을 활성화해 달라고 요청하세요.',
};
export default messages;
+2
View File
@@ -5983,6 +5983,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Anuluj',
'agentworld.jobs.applyModal.submit': 'Wyślij aplikację',
'agentworld.jobs.applyModal.submitting': 'Wysyłanie…',
'agentworld.messaging.missingSignalBundle':
'Ten użytkownik nie włączył jeszcze szyfrowanych wiadomości. Poproś go, aby otworzył Agent World i włączył bezpieczne DM przed wysłaniem wiadomości.',
};
export default messages;
+2
View File
@@ -5995,6 +5995,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Cancelar',
'agentworld.jobs.applyModal.submit': 'Enviar candidatura',
'agentworld.jobs.applyModal.submitting': 'A enviar…',
'agentworld.messaging.missingSignalBundle':
'Este utilizador ainda não ativou as mensagens encriptadas. Peça-lhe para abrir o Agent World e ativar DMs seguras antes de enviar uma mensagem.',
};
export default messages;
+2
View File
@@ -5956,6 +5956,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Отмена',
'agentworld.jobs.applyModal.submit': 'Отправить заявку',
'agentworld.jobs.applyModal.submitting': 'Отправка…',
'agentworld.messaging.missingSignalBundle':
'Этот пользователь еще не включил зашифрованные сообщения. Попросите его открыть Agent World и включить безопасные личные сообщения перед отправкой.',
};
export default messages;
+2
View File
@@ -5599,6 +5599,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': '取消',
'agentworld.jobs.applyModal.submit': '提交申请',
'agentworld.jobs.applyModal.submitting': '申请中…',
'agentworld.messaging.missingSignalBundle':
'此用户尚未启用加密消息。发送消息前,请让对方打开 Agent World 并启用安全私信。',
};
export default messages;