mirror of
https://github.com/block/buzz.git
synced 2026-11-07 18:01:06 +00:00
fix(mobile): label Solo agents with the owner's inventory name
Solo surfaces showed the agent's self-authored kind:0 display name, so an agent could rename itself in the owner's picker and chat header. Use the name from the owner's 30177 record, as desktop's managed-agent directory does, and keep the profile for NIP-OA verification and other metadata. Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -55,7 +55,8 @@ Future<List<AgentDirectoryEntry>> _loadAssistants(
|
||||
RelaySessionNotifier session, {
|
||||
String? pubkey,
|
||||
}) async {
|
||||
final candidates = await _managedAgentPubkeys(sync, session, agent: pubkey);
|
||||
final names = await _managedAgentNames(sync, session, agent: pubkey);
|
||||
final candidates = names.keys.toList();
|
||||
final profiles = <String, NostrEvent>{};
|
||||
for (var offset = 0; offset < candidates.length; offset += _pageSize) {
|
||||
final batch = candidates.sublist(
|
||||
@@ -84,7 +85,18 @@ Future<List<AgentDirectoryEntry>> _loadAssistants(
|
||||
for (final event in profiles.values) {
|
||||
try {
|
||||
if (verifiedProfileOaOwnerPubkey(event) == sync.pubkey) {
|
||||
agents.add(AgentDirectoryEntry.fromEvent(event));
|
||||
// The owner's inventory name is authoritative on Solo surfaces; the
|
||||
// agent's self-authored profile cannot rename itself there.
|
||||
final entry = AgentDirectoryEntry.fromEvent(event);
|
||||
agents.add(
|
||||
AgentDirectoryEntry(
|
||||
pubkey: entry.pubkey,
|
||||
displayName: names[entry.pubkey],
|
||||
respondTo: entry.respondTo,
|
||||
respondToAllowlist: entry.respondToAllowlist,
|
||||
channelIds: entry.channelIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
// A malformed profile must not hide unrelated verified agents.
|
||||
@@ -94,12 +106,12 @@ Future<List<AgentDirectoryEntry>> _loadAssistants(
|
||||
return agents;
|
||||
}
|
||||
|
||||
/// Candidate agents from the owner's own managed-agent records, as desktop
|
||||
/// discovers them. The records are discovery hints only: each candidate's
|
||||
/// Candidate agents and their owner-assigned names from the owner's own
|
||||
/// managed-agent records, as desktop discovers them. The records are discovery hints only: each candidate's
|
||||
/// profile must still carry a valid NIP-OA attestation by the owner. With
|
||||
/// [agent], only that agent's record is read, so an agent removed from the
|
||||
/// inventory also fails revalidation before chat creation and send.
|
||||
Future<List<String>> _managedAgentPubkeys(
|
||||
Future<Map<String, String>> _managedAgentNames(
|
||||
MeSync sync,
|
||||
RelaySessionNotifier session, {
|
||||
String? agent,
|
||||
@@ -161,34 +173,35 @@ Future<List<String>> _managedAgentPubkeys(
|
||||
}
|
||||
cursor = last;
|
||||
}
|
||||
return [
|
||||
return {
|
||||
for (final MapEntry(key: d, value: event) in heads.entries)
|
||||
if (_agentPubkey.hasMatch(d) && _isSupportedPolicy(event)) d,
|
||||
];
|
||||
if (_agentPubkey.hasMatch(d)) d: ?_supportedPolicyName(event),
|
||||
};
|
||||
}
|
||||
|
||||
bool _newer(NostrEvent a, NostrEvent b) =>
|
||||
a.createdAt > b.createdAt ||
|
||||
(a.createdAt == b.createdAt && a.id.compareTo(b.id) < 0);
|
||||
|
||||
/// Whether a managed-agent record holds a policy desktop can apply. This
|
||||
/// The owner-assigned name of a managed-agent record whose policy desktop can
|
||||
/// apply, or null. This
|
||||
/// mirrors desktop's `ManagedAgentEventContent` parse, plus the non-empty name
|
||||
/// desktop's agent library requires. An unsupported latest record fails
|
||||
/// closed: it excludes the agent rather than falling back to an older one.
|
||||
bool _isSupportedPolicy(NostrEvent event) {
|
||||
String? _supportedPolicyName(NostrEvent event) {
|
||||
final Object? decoded;
|
||||
try {
|
||||
decoded = jsonDecode(event.content);
|
||||
} on FormatException {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
if (decoded is! Map<String, dynamic>) return false;
|
||||
if (decoded is! Map<String, dynamic>) return null;
|
||||
final body = decoded;
|
||||
final name = body['name'];
|
||||
if (name is! String || name.trim().isEmpty) return null;
|
||||
final parallelism = body['parallelism'];
|
||||
final allowlist = body['respond_to_allowlist'];
|
||||
return name is String &&
|
||||
name.trim().isNotEmpty &&
|
||||
final supported =
|
||||
parallelism is int &&
|
||||
parallelism >= 0 &&
|
||||
parallelism <= 0xFFFFFFFF &&
|
||||
@@ -199,6 +212,7 @@ bool _isSupportedPolicy(NostrEvent event) {
|
||||
final value = body[field];
|
||||
return value == null || value is String;
|
||||
});
|
||||
return supported ? name.trim() : null;
|
||||
}
|
||||
|
||||
const _respondTo = {'owner-only', 'allowlist', 'anyone'};
|
||||
|
||||
@@ -157,7 +157,7 @@ void main() {
|
||||
);
|
||||
final choices = await setup(relay).read(meAssistantsProvider.future);
|
||||
expect(choices.map((a) => a.pubkey), [agent.public]);
|
||||
expect(choices.single.displayName, 'My assistant');
|
||||
expect(choices.single.displayName, 'Agent');
|
||||
expect(relay.inventoryFilters.single, {
|
||||
'kinds': [30177],
|
||||
'authors': [owner.public],
|
||||
@@ -169,6 +169,24 @@ void main() {
|
||||
);
|
||||
expect(relay.profileFilters.single['limit'], 3);
|
||||
});
|
||||
test('the owner inventory name outranks the agent profile name', () async {
|
||||
final relay = _Relay(
|
||||
inventory: [
|
||||
[
|
||||
managed(agent.public, content: {...policy, 'name': ' Owner Name '}),
|
||||
],
|
||||
],
|
||||
profiles: [
|
||||
profile(
|
||||
agent,
|
||||
ownedBy: owner,
|
||||
content: {'display_name': 'Confusable Name', 'name': 'Other'},
|
||||
),
|
||||
],
|
||||
);
|
||||
final choices = await setup(relay).read(meAssistantsProvider.future);
|
||||
expect(choices.single.displayName, 'Owner Name');
|
||||
});
|
||||
test('does not query profiles without managed agents', () async {
|
||||
final relay = _Relay(profiles: [profile(agent, ownedBy: owner)]);
|
||||
expect(await setup(relay).read(meAssistantsProvider.future), isEmpty);
|
||||
@@ -206,7 +224,7 @@ void main() {
|
||||
final choices = await container.read(meAssistantsProvider.future);
|
||||
expect(
|
||||
choices.singleWhere((a) => a.pubkey == agent.public).displayName,
|
||||
'My assistant',
|
||||
'Agent',
|
||||
);
|
||||
expect(
|
||||
await isOwnedMeAssistant(
|
||||
|
||||
Reference in New Issue
Block a user