From d26c15ae15db8dafd4ed1c30a03996826765d2e7 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Mon, 3 Aug 2026 03:20:11 -0400 Subject: [PATCH] test(desktop): add two-workspace relay partition e2e probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../tests/e2e_managed_agent.rs | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/crates/buzz-test-client/tests/e2e_managed_agent.rs b/crates/buzz-test-client/tests/e2e_managed_agent.rs index ca5373a2e..fc3fd169e 100644 --- a/crates/buzz-test-client/tests/e2e_managed_agent.rs +++ b/crates/buzz-test-client/tests/e2e_managed_agent.rs @@ -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//`. +#[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"); +}