test(desktop): add two-workspace relay partition e2e probe

Adds test_two_workspace_relay_partition to the managed-agent e2e suite.

The test models workspace A and workspace B as two distinct owner keypairs
on the same relay and verifies in both directions:

1. Owner A's NIP-33 author-scoped subscription returns only A's definition,
   not B's (workspace B content never leaks into A's view).
2. Owner B's subscription is symmetric — returns only B's definition.
3. Cross-scope queries prove NIP-33 (kind, author, d-tag) scoping: two
   owners publishing under the same d-tag get distinct relay coordinates
   that cannot collide or bleed across.

This is the relay-level half of the live two-workspace leak probe required
by the workspace-scoped agent definition store (PR #4485, plan v4 Phase 4).
The filesystem-level half is covered by scope_id unit tests confirming that
distinct (relay_url, owner_pubkey) pairs always produce distinct scope_id
directories under agents/scopes/<scope_id>/.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-08-03 03:20:11 -04:00
co-authored by Will Pfleger
parent d8ee40814e
commit d26c15ae15
@@ -366,3 +366,191 @@ async fn test_managed_agent_tombstone_deletes_coordinate() {
client.disconnect().await.expect("disconnect");
}
/// Two-workspace relay partition probe.
///
/// Workspace A and workspace B are modelled as two distinct owner keypairs on
/// the same relay. This test verifies:
///
/// 1. Owner A's events are author-scoped: a subscription filtered by
/// `author: owner_a` returns only owner_a's events, not owner_b's.
/// 2. Symmetrically, owner B's subscription returns only owner_b's events.
/// 3. Cross-author subscriptions return zero events for the other owner's
/// d-tag coordinate (NIP-33 scoping by `(kind, author, d-tag)` means
/// the coordinate for owner_a's agent and the coordinate for owner_b's
/// agent are disjoint — same d-tag value, but different author pubkeys
/// produce different NIP-33 addresses).
///
/// This is the relay-level half of the two-workspace isolation proof. The
/// filesystem half is covered by the scope_id unit tests: different
/// `(relay_url, owner_pubkey)` pairs always produce distinct scope_id
/// directories under `agents/scopes/<scope_id>/`.
#[tokio::test]
#[ignore]
async fn test_two_workspace_relay_partition() {
let url = relay_url();
// Workspace A and workspace B: two distinct owner keypairs (same relay)
let owner_a_keys = Keys::generate();
let owner_b_keys = Keys::generate();
// Use the same d-tag value (simulating same agent slug) in both workspaces.
// Relay NIP-33 addressing is (kind, author, d-tag) — so same d-tag but
// different authors are distinct coordinates that cannot collide.
let shared_d_tag = "workspace-leak-probe-agent";
// Publish agent definition as owner A
let mut client_a = BuzzTestClient::connect(&url, &owner_a_keys)
.await
.expect("owner_a connect");
let content_a = agent_projection_content("WorkspaceA-ExclusiveAgent");
let event_a = EventBuilder::new(Kind::Custom(AGENT_KIND), content_a.clone())
.tag(Tag::identifier(shared_d_tag))
.sign_with_keys(&owner_a_keys)
.expect("owner_a sign");
let ok_a = client_a
.send_event(event_a)
.await
.expect("send owner_a event");
assert!(
ok_a.accepted,
"relay rejected owner_a's event: {}",
ok_a.message
);
// Publish agent definition as owner B (same relay, different owner)
let mut client_b = BuzzTestClient::connect(&url, &owner_b_keys)
.await
.expect("owner_b connect");
let content_b = agent_projection_content("WorkspaceB-ExclusiveAgent");
let event_b = EventBuilder::new(Kind::Custom(AGENT_KIND), content_b.clone())
.tag(Tag::identifier(shared_d_tag))
.sign_with_keys(&owner_b_keys)
.expect("owner_b sign");
let ok_b = client_b
.send_event(event_b)
.await
.expect("send owner_b event");
assert!(
ok_b.accepted,
"relay rejected owner_b's event: {}",
ok_b.message
);
// ── Direction 1: owner_a's author-scoped subscription ──────────────────
// Owner A subscribes to their own agent coordinate.
// Must see exactly their definition, not owner_b's.
let sid_a = sub_id("probe-workspace-a");
let filter_a = Filter::new()
.kind(Kind::Custom(AGENT_KIND))
.author(owner_a_keys.public_key())
.custom_tags(SingleLetterTag::lowercase(Alphabet::D), [shared_d_tag]);
client_a
.subscribe(&sid_a, vec![filter_a])
.await
.expect("owner_a subscribe");
let events_a = client_a
.collect_until_eose(&sid_a, Duration::from_secs(5))
.await
.expect("owner_a collect");
assert_eq!(
events_a.len(),
1,
"owner_a's NIP-33 subscription must return exactly 1 event (their own), got {}",
events_a.len()
);
assert!(
events_a[0].content.contains("WorkspaceA-ExclusiveAgent"),
"owner_a's event must contain workspace-A content, got: {}",
events_a[0].content
);
assert_eq!(
events_a[0].pubkey,
owner_a_keys.public_key(),
"owner_a's subscription must not return events from owner_b"
);
assert!(
!events_a[0].content.contains("WorkspaceB-ExclusiveAgent"),
"workspace A's subscription must NOT return workspace B's content"
);
// ── Direction 2: owner_b's author-scoped subscription ──────────────────
// Symmetric: owner B must see only their definition.
let sid_b = sub_id("probe-workspace-b");
let filter_b = Filter::new()
.kind(Kind::Custom(AGENT_KIND))
.author(owner_b_keys.public_key())
.custom_tags(SingleLetterTag::lowercase(Alphabet::D), [shared_d_tag]);
client_b
.subscribe(&sid_b, vec![filter_b])
.await
.expect("owner_b subscribe");
let events_b = client_b
.collect_until_eose(&sid_b, Duration::from_secs(5))
.await
.expect("owner_b collect");
assert_eq!(
events_b.len(),
1,
"owner_b's NIP-33 subscription must return exactly 1 event (their own), got {}",
events_b.len()
);
assert!(
events_b[0].content.contains("WorkspaceB-ExclusiveAgent"),
"owner_b's event must contain workspace-B content, got: {}",
events_b[0].content
);
assert_eq!(
events_b[0].pubkey,
owner_b_keys.public_key(),
"owner_b's subscription must not return events from owner_a"
);
assert!(
!events_b[0].content.contains("WorkspaceA-ExclusiveAgent"),
"workspace B's subscription must NOT return workspace A's content"
);
// ── Direction 3: cross-scope subscription returns zero (NIP-33 isolation) ──
// Owner A subscribes to the same d-tag but filtered by owner_b's pubkey.
// This is the precise test that a workspace switching to B cannot accidentally
// read A's NIP-33 coordinates: the (kind=30177, author=owner_b, d=shared_d_tag)
// coordinate resolves to B's definition, not A's.
let sid_cross = sub_id("probe-cross-scope");
let filter_cross = Filter::new()
.kind(Kind::Custom(AGENT_KIND))
.author(owner_b_keys.public_key()) // owner_b's pubkey
.custom_tags(SingleLetterTag::lowercase(Alphabet::D), [shared_d_tag]);
client_a
.subscribe(&sid_cross, vec![filter_cross])
.await
.expect("cross-scope subscribe");
let events_cross = client_a
.collect_until_eose(&sid_cross, Duration::from_secs(5))
.await
.expect("cross-scope collect");
// The cross-scope query must return B's event (by B's pubkey), not A's.
// This confirms NIP-33 coordinates are scoped by (kind, author, d-tag).
assert_eq!(
events_cross.len(),
1,
"cross-scope query must return exactly 1 event (B's own), got {}",
events_cross.len()
);
assert_eq!(
events_cross[0].pubkey,
owner_b_keys.public_key(),
"cross-scope query must return B's event, not A's"
);
assert!(
!events_cross[0]
.content
.contains("WorkspaceA-ExclusiveAgent"),
"cross-scope query must NOT return workspace A's definitions"
);
client_a.disconnect().await.expect("owner_a disconnect");
client_b.disconnect().await.expect("owner_b disconnect");
}