diff --git a/mobile/lib/shared/agents/owned_agent_directory.dart b/mobile/lib/shared/agents/owned_agent_directory.dart index e8d6f347ea..bf11a50c5a 100644 --- a/mobile/lib/shared/agents/owned_agent_directory.dart +++ b/mobile/lib/shared/agents/owned_agent_directory.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'package:nostr/nostr.dart' as nostr; + import 'package:hooks_riverpod/hooks_riverpod.dart'; +import '../crypto/event_signing.dart'; import '../crypto/nip_oa.dart'; import '../mentions/agent_identity_provider.dart'; import '../relay/relay.dart'; @@ -99,7 +100,9 @@ Future> loadOwnedAgentDirectory( String? model; try { final registration = registrations[event.pubkey]!; - nostr.Event.fromJson(jsonEncode(registration.toJson())); + // Native libsecp256k1 verification; the pure-Dart codec took + // several milliseconds per registration on the UI isolate. + verifiedNostrEventFromJson(jsonEncode(registration.toJson())); final data = jsonDecode(registration.content); if (data is Map && data['model'] is String) { final value = (data['model'] as String).trim(); diff --git a/mobile/test/shared/agents/owned_agent_directory_test.dart b/mobile/test/shared/agents/owned_agent_directory_test.dart index 7636e5e72b..17b0f0188b 100644 --- a/mobile/test/shared/agents/owned_agent_directory_test.dart +++ b/mobile/test/shared/agents/owned_agent_directory_test.dart @@ -3,6 +3,7 @@ import 'package:buzz/shared/relay/relay.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nostr/nostr.dart' as nostr; + import '../../helpers/remote_agents_fixture.dart'; NostrEvent registration( @@ -137,6 +138,40 @@ void main() { ]); }); + test('a registration whose signature fails keeps the agent without its ' + 'model', () async { + final signed = registration( + fakeAgent, + content: '{"name":"Remote","model":"Claude Sonnet"}', + ); + // Same id and content, but another key's signature over the id. + final forged = NostrEvent.fromJson({ + ...signed.toJson(), + 'sig': nostr.Event.from( + kind: 1, + content: 'other', + secretKey: fakeAgentKeys.secret, + ).sig, + }); + final profiles = [ + signedAgentProfile( + fakeAgentKeys, + fakeOwner, + content: '{"name":"Remote"}', + ), + ]; + Future> load(NostrEvent event) => + loadOwnedAgentDirectory( + fakeOwner.public, + (filter) async => filter.kinds.contains(30177) ? [event] : profiles, + ); + + expect((await load(signed)).single.model, 'Claude Sonnet'); + final found = (await load(forged)).single; + expect(found.pubkey, fakeAgent); + expect(found.model, isNull); + }); + test('a registration is listed only with a type runtime and a name', () { final agent = fakeAgent; final cases = <(String, NostrEvent, bool)>[